diff --git a/modules/base/tool.go b/modules/base/tool.go
index 168a2220b..231507546 100644
--- a/modules/base/tool.go
+++ b/modules/base/tool.go
@@ -174,7 +174,7 @@ func Int64sToStrings(ints []int64) []string {
 func EntryIcon(entry *git.TreeEntry) string {
 	switch {
 	case entry.IsLink():
-		te, err := entry.FollowLink()
+		te, _, err := entry.FollowLink()
 		if err != nil {
 			log.Debug(err.Error())
 			return "file-symlink-file"
diff --git a/modules/git/tree_entry.go b/modules/git/tree_entry.go
index 951312148..2c47c8858 100644
--- a/modules/git/tree_entry.go
+++ b/modules/git/tree_entry.go
@@ -23,15 +23,15 @@ func (te *TreeEntry) Type() string {
 }
 
 // FollowLink returns the entry pointed to by a symlink
-func (te *TreeEntry) FollowLink() (*TreeEntry, error) {
+func (te *TreeEntry) FollowLink() (*TreeEntry, string, error) {
 	if !te.IsLink() {
-		return nil, ErrBadLink{te.Name(), "not a symlink"}
+		return nil, "", ErrBadLink{te.Name(), "not a symlink"}
 	}
 
 	// read the link
 	r, err := te.Blob().DataAsync()
 	if err != nil {
-		return nil, err
+		return nil, "", err
 	}
 	closed := false
 	defer func() {
@@ -42,7 +42,7 @@ func (te *TreeEntry) FollowLink() (*TreeEntry, error) {
 	buf := make([]byte, te.Size())
 	_, err = io.ReadFull(r, buf)
 	if err != nil {
-		return nil, err
+		return nil, "", err
 	}
 	_ = r.Close()
 	closed = true
@@ -56,33 +56,35 @@ func (te *TreeEntry) FollowLink() (*TreeEntry, error) {
 	}
 
 	if t == nil {
-		return nil, ErrBadLink{te.Name(), "points outside of repo"}
+		return nil, "", ErrBadLink{te.Name(), "points outside of repo"}
 	}
 
 	target, err := t.GetTreeEntryByPath(lnk)
 	if err != nil {
 		if IsErrNotExist(err) {
-			return nil, ErrBadLink{te.Name(), "broken link"}
+			return nil, "", ErrBadLink{te.Name(), "broken link"}
 		}
-		return nil, err
+		return nil, "", err
 	}
-	return target, nil
+	return target, lnk, nil
 }
 
 // FollowLinks returns the entry ultimately pointed to by a symlink
-func (te *TreeEntry) FollowLinks() (*TreeEntry, error) {
+func (te *TreeEntry) FollowLinks() (*TreeEntry, string, error) {
 	if !te.IsLink() {
-		return nil, ErrBadLink{te.Name(), "not a symlink"}
+		return nil, "", ErrBadLink{te.Name(), "not a symlink"}
 	}
 	entry := te
+	entryLink := ""
 	for i := 0; i < 999; i++ {
 		if entry.IsLink() {
-			next, err := entry.FollowLink()
+			next, link, err := entry.FollowLink()
+			entryLink = link
 			if err != nil {
-				return nil, err
+				return nil, "", err
 			}
 			if next.ID == entry.ID {
-				return nil, ErrBadLink{
+				return nil, "", ErrBadLink{
 					entry.Name(),
 					"recursive link",
 				}
@@ -93,12 +95,12 @@ func (te *TreeEntry) FollowLinks() (*TreeEntry, error) {
 		}
 	}
 	if entry.IsLink() {
-		return nil, ErrBadLink{
+		return nil, "", ErrBadLink{
 			te.Name(),
 			"too many levels of symbolic links",
 		}
 	}
-	return entry, nil
+	return entry, entryLink, nil
 }
 
 // returns the Tree pointed to by this TreeEntry, or nil if this is not a tree
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 0a6b1e0d8..424b06379 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1205,6 +1205,7 @@ tag = Tag
 released_this = released this
 file.title = %s at %s
 file_raw = Raw
+file_follow = Follow Symlink
 file_history = History
 file_view_source = View Source
 file_view_rendered = View Rendered
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index 86977062c..32c09bb5f 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -114,7 +114,7 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try
 			log.Debug("Potential readme file: %s", entry.Name())
 			if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].Name(), entry.Blob().Name()) {
 				if entry.IsLink() {
-					target, err := entry.FollowLinks()
+					target, _, err := entry.FollowLinks()
 					if err != nil && !git.IsErrBadLink(err) {
 						return "", nil, err
 					} else if target != nil && (target.IsExecutable() || target.IsRegular()) {
@@ -267,7 +267,7 @@ func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte,
 func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.TreeEntry) {
 	target := readmeFile
 	if readmeFile != nil && readmeFile.IsLink() {
-		target, _ = readmeFile.FollowLinks()
+		target, _, _ = readmeFile.FollowLinks()
 	}
 	if target == nil {
 		// if findReadmeFile() failed and/or gave us a broken symlink (which it shouldn't)
@@ -391,6 +391,15 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
 	ctx.Data["FileName"] = blob.Name()
 	ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
 
+	if entry.IsLink() {
+		_, link, err := entry.FollowLinks()
+		// Errors should be allowed, because this shouldn't
+		// block rendering invalid symlink files.
+		if err == nil {
+			ctx.Data["SymlinkURL"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(link)
+		}
+	}
+
 	commit, err := ctx.Repo.Commit.GetCommitByPath(ctx.Repo.TreePath)
 	if err != nil {
 		ctx.ServerError("GetCommitByPath", err)
diff --git a/templates/repo/view_file.tmpl b/templates/repo/view_file.tmpl
index b8eb2393f..91b10f744 100644
--- a/templates/repo/view_file.tmpl
+++ b/templates/repo/view_file.tmpl
@@ -43,6 +43,9 @@
 			{{end}}
 			{{if not .ReadmeInList}}
 				<div class="ui buttons gt-mr-2">
+					{{if .SymlinkURL}}
+						<a class="ui mini basic button" href="{{$.SymlinkURL}}" data-kind="follow-symlink">{{ctx.Locale.Tr "repo.file_follow"}}</a>
+					{{end}}
 					<a class="ui mini basic button" href="{{$.RawFileLink}}">{{ctx.Locale.Tr "repo.file_raw"}}</a>
 					{{if not .IsViewCommit}}
 						<a class="ui mini basic button" href="{{.RepoLink}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.file_permalink"}}</a>
diff --git a/tests/integration/repo_test.go b/tests/integration/repo_test.go
index 03124ecaf..cb79a2fa9 100644
--- a/tests/integration/repo_test.go
+++ b/tests/integration/repo_test.go
@@ -961,3 +961,54 @@ func TestRepoFilesList(t *testing.T) {
 		assert.EqualValues(t, []string{"Charlie", "alpha", "Beta", "delta", "licensa", "LICENSE", "licensz", "README.md", "zEta"}, filesList)
 	})
 }
+
+func TestRepoFollowSymlink(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+	session := loginUser(t, "user2")
+
+	assertCase := func(t *testing.T, url, expectedSymlinkURL string, shouldExist bool) {
+		t.Helper()
+
+		req := NewRequest(t, "GET", url)
+		resp := session.MakeRequest(t, req, http.StatusOK)
+
+		htmlDoc := NewHTMLParser(t, resp.Body)
+		symlinkURL, ok := htmlDoc.Find(".file-actions .button[data-kind='follow-symlink']").Attr("href")
+		if shouldExist {
+			assert.True(t, ok)
+			assert.EqualValues(t, expectedSymlinkURL, symlinkURL)
+		} else {
+			assert.False(t, ok)
+		}
+	}
+
+	t.Run("Normal", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+		assertCase(t, "/user2/readme-test/src/branch/symlink/README.md?display=source", "/user2/readme-test/src/branch/symlink/some/other/path/awefulcake.txt", true)
+	})
+
+	t.Run("Normal", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+		assertCase(t, "/user2/readme-test/src/branch/symlink/some/README.txt", "/user2/readme-test/src/branch/symlink/some/other/path/awefulcake.txt", true)
+	})
+
+	t.Run("Normal", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+		assertCase(t, "/user2/readme-test/src/branch/symlink/up/back/down/down/README.md", "/user2/readme-test/src/branch/symlink/down/side/../left/right/../reelmein", true)
+	})
+
+	t.Run("Broken symlink", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+		assertCase(t, "/user2/readme-test/src/branch/fallbacks-broken-symlinks/docs/README", "", false)
+	})
+
+	t.Run("Loop symlink", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+		assertCase(t, "/user2/readme-test/src/branch/symlink-loop/README.md", "", false)
+	})
+
+	t.Run("Not a symlink", func(t *testing.T) {
+		defer tests.PrintCurrentTest(t)()
+		assertCase(t, "/user2/readme-test/src/branch/master/README.md", "", false)
+	})
+}