diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go
index 300357e1ec..1b1aba17d9 100644
--- a/routers/api/v1/repo/pull.go
+++ b/routers/api/v1/repo/pull.go
@@ -815,7 +815,7 @@ func MergePullRequest(ctx *context.APIContext) {
 
 	message := strings.TrimSpace(form.MergeTitleField)
 	if len(message) == 0 {
-		message, err = pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pr, repo_model.MergeStyle(form.Do))
+		message, _, err = pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pr, repo_model.MergeStyle(form.Do))
 		if err != nil {
 			ctx.Error(http.StatusInternalServerError, "GetDefaultMergeMessage", err)
 			return
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index 79a8b2745a..100e343de4 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -1664,19 +1664,21 @@ func ViewIssue(ctx *context.Context) {
 
 		ctx.Data["MergeStyle"] = mergeStyle
 
-		defaultMergeMessage, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, mergeStyle)
+		defaultMergeMessage, defaultMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, mergeStyle)
 		if err != nil {
 			ctx.ServerError("GetDefaultMergeMessage", err)
 			return
 		}
 		ctx.Data["DefaultMergeMessage"] = defaultMergeMessage
+		ctx.Data["DefaultMergeBody"] = defaultMergeBody
 
-		defaultSquashMergeMessage, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, repo_model.MergeStyleSquash)
+		defaultSquashMergeMessage, defaultSquashMergeBody, err := pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pull, repo_model.MergeStyleSquash)
 		if err != nil {
 			ctx.ServerError("GetDefaultSquashMergeMessage", err)
 			return
 		}
 		ctx.Data["DefaultSquashMergeMessage"] = defaultSquashMergeMessage
+		ctx.Data["DefaultSquashMergeBody"] = defaultSquashMergeBody
 
 		if err = pull.LoadProtectedBranch(ctx); err != nil {
 			ctx.ServerError("LoadProtectedBranch", err)
diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go
index 8929a183ee..a18f9f6a56 100644
--- a/routers/web/repo/pull.go
+++ b/routers/web/repo/pull.go
@@ -986,7 +986,7 @@ func MergePullRequest(ctx *context.Context) {
 	message := strings.TrimSpace(form.MergeTitleField)
 	if len(message) == 0 {
 		var err error
-		message, err = pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pr, repo_model.MergeStyle(form.Do))
+		message, _, err = pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pr, repo_model.MergeStyle(form.Do))
 		if err != nil {
 			ctx.ServerError("GetDefaultMergeMessage", err)
 			return
diff --git a/services/pull/merge.go b/services/pull/merge.go
index 41ba45c177..7a936163f1 100644
--- a/services/pull/merge.go
+++ b/services/pull/merge.go
@@ -39,19 +39,19 @@ import (
 )
 
 // GetDefaultMergeMessage returns default message used when merging pull request
-func GetDefaultMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr *issues_model.PullRequest, mergeStyle repo_model.MergeStyle) (string, error) {
+func GetDefaultMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr *issues_model.PullRequest, mergeStyle repo_model.MergeStyle) (message, body string, err error) {
 	if err := pr.LoadHeadRepo(ctx); err != nil {
-		return "", err
+		return "", "", err
 	}
 	if err := pr.LoadBaseRepo(ctx); err != nil {
-		return "", err
+		return "", "", err
 	}
 	if pr.BaseRepo == nil {
-		return "", repo_model.ErrRepoNotExist{ID: pr.BaseRepoID}
+		return "", "", repo_model.ErrRepoNotExist{ID: pr.BaseRepoID}
 	}
 
 	if err := pr.LoadIssue(ctx); err != nil {
-		return "", err
+		return "", "", err
 	}
 
 	isExternalTracker := pr.BaseRepo.UnitEnabled(ctx, unit.TypeExternalTracker)
@@ -64,12 +64,12 @@ func GetDefaultMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr
 		templateFilepath := fmt.Sprintf(".gitea/default_merge_message/%s_TEMPLATE.md", strings.ToUpper(string(mergeStyle)))
 		commit, err := baseGitRepo.GetBranchCommit(pr.BaseRepo.DefaultBranch)
 		if err != nil {
-			return "", err
+			return "", "", err
 		}
 		templateContent, err := commit.GetFileContent(templateFilepath, setting.Repository.PullRequest.DefaultMergeMessageSize)
 		if err != nil {
 			if !git.IsErrNotExist(err) {
-				return "", err
+				return "", "", err
 			}
 		} else {
 			vars := map[string]string{
@@ -107,27 +107,35 @@ func GetDefaultMergeMessage(ctx context.Context, baseGitRepo *git.Repository, pr
 					vars["ClosingIssues"] = ""
 				}
 			}
-
-			return os.Expand(templateContent, func(s string) string {
-				return vars[s]
-			}), nil
+			message, body = expandDefaultMergeMessage(templateContent, vars)
+			return message, body, nil
 		}
 	}
 
 	// Squash merge has a different from other styles.
 	if mergeStyle == repo_model.MergeStyleSquash {
-		return fmt.Sprintf("%s (%s%d)", pr.Issue.Title, issueReference, pr.Issue.Index), nil
+		return fmt.Sprintf("%s (%s%d)", pr.Issue.Title, issueReference, pr.Issue.Index), "", nil
 	}
 
 	if pr.BaseRepoID == pr.HeadRepoID {
-		return fmt.Sprintf("Merge pull request '%s' (%s%d) from %s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadBranch, pr.BaseBranch), nil
+		return fmt.Sprintf("Merge pull request '%s' (%s%d) from %s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadBranch, pr.BaseBranch), "", nil
 	}
 
 	if pr.HeadRepo == nil {
-		return fmt.Sprintf("Merge pull request '%s' (%s%d) from <deleted>:%s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadBranch, pr.BaseBranch), nil
+		return fmt.Sprintf("Merge pull request '%s' (%s%d) from <deleted>:%s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadBranch, pr.BaseBranch), "", nil
 	}
 
-	return fmt.Sprintf("Merge pull request '%s' (%s%d) from %s:%s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseBranch), nil
+	return fmt.Sprintf("Merge pull request '%s' (%s%d) from %s:%s into %s", pr.Issue.Title, issueReference, pr.Issue.Index, pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseBranch), "", nil
+}
+
+func expandDefaultMergeMessage(template string, vars map[string]string) (message, body string) {
+	message = strings.TrimSpace(template)
+	if splits := strings.SplitN(message, "\n", 2); len(splits) == 2 {
+		message = splits[0]
+		body = strings.TrimSpace(splits[1])
+	}
+	mapping := func(s string) string { return vars[s] }
+	return os.Expand(message, mapping), os.Expand(body, mapping)
 }
 
 // Merge merges pull request to base repository.
diff --git a/services/pull/merge_test.go b/services/pull/merge_test.go
new file mode 100644
index 0000000000..6df6f55d46
--- /dev/null
+++ b/services/pull/merge_test.go
@@ -0,0 +1,67 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package pull
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_expandDefaultMergeMessage(t *testing.T) {
+	type args struct {
+		template string
+		vars     map[string]string
+	}
+	tests := []struct {
+		name     string
+		args     args
+		want     string
+		wantBody string
+	}{
+		{
+			name: "single line",
+			args: args{
+				template: "Merge ${PullRequestTitle}",
+				vars: map[string]string{
+					"PullRequestTitle":       "PullRequestTitle",
+					"PullRequestDescription": "Pull\nRequest\nDescription\n",
+				},
+			},
+			want:     "Merge PullRequestTitle",
+			wantBody: "",
+		},
+		{
+			name: "multiple lines",
+			args: args{
+				template: "Merge ${PullRequestTitle}\nDescription:\n\n${PullRequestDescription}\n",
+				vars: map[string]string{
+					"PullRequestTitle":       "PullRequestTitle",
+					"PullRequestDescription": "Pull\nRequest\nDescription\n",
+				},
+			},
+			want:     "Merge PullRequestTitle",
+			wantBody: "Description:\n\nPull\nRequest\nDescription\n",
+		},
+		{
+			name: "leading newlines",
+			args: args{
+				template: "\n\n\nMerge ${PullRequestTitle}\n\n\nDescription:\n\n${PullRequestDescription}\n",
+				vars: map[string]string{
+					"PullRequestTitle":       "PullRequestTitle",
+					"PullRequestDescription": "Pull\nRequest\nDescription\n",
+				},
+			},
+			want:     "Merge PullRequestTitle",
+			wantBody: "Description:\n\nPull\nRequest\nDescription\n",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, got1 := expandDefaultMergeMessage(tt.args.template, tt.args.vars)
+			assert.Equalf(t, tt.want, got, "expandDefaultMergeMessage(%v, %v)", tt.args.template, tt.args.vars)
+			assert.Equalf(t, tt.wantBody, got1, "expandDefaultMergeMessage(%v, %v)", tt.args.template, tt.args.vars)
+		})
+	}
+}
diff --git a/services/pull/pull_test.go b/services/pull/pull_test.go
index cbbdccce9c..d63227a7d5 100644
--- a/services/pull/pull_test.go
+++ b/services/pull/pull_test.go
@@ -45,13 +45,13 @@ func TestPullRequest_GetDefaultMergeMessage_InternalTracker(t *testing.T) {
 	assert.NoError(t, err)
 	defer gitRepo.Close()
 
-	mergeMessage, err := GetDefaultMergeMessage(db.DefaultContext, gitRepo, pr, "")
+	mergeMessage, _, err := GetDefaultMergeMessage(db.DefaultContext, gitRepo, pr, "")
 	assert.NoError(t, err)
 	assert.Equal(t, "Merge pull request 'issue3' (#3) from branch2 into master", mergeMessage)
 
 	pr.BaseRepoID = 1
 	pr.HeadRepoID = 2
-	mergeMessage, err = GetDefaultMergeMessage(db.DefaultContext, gitRepo, pr, "")
+	mergeMessage, _, err = GetDefaultMergeMessage(db.DefaultContext, gitRepo, pr, "")
 	assert.NoError(t, err)
 	assert.Equal(t, "Merge pull request 'issue3' (#3) from user2/repo1:branch2 into master", mergeMessage)
 }
@@ -75,7 +75,7 @@ func TestPullRequest_GetDefaultMergeMessage_ExternalTracker(t *testing.T) {
 	assert.NoError(t, err)
 	defer gitRepo.Close()
 
-	mergeMessage, err := GetDefaultMergeMessage(db.DefaultContext, gitRepo, pr, "")
+	mergeMessage, _, err := GetDefaultMergeMessage(db.DefaultContext, gitRepo, pr, "")
 	assert.NoError(t, err)
 
 	assert.Equal(t, "Merge pull request 'issue3' (!3) from branch2 into master", mergeMessage)
@@ -84,7 +84,7 @@ func TestPullRequest_GetDefaultMergeMessage_ExternalTracker(t *testing.T) {
 	pr.HeadRepoID = 2
 	pr.BaseRepo = nil
 	pr.HeadRepo = nil
-	mergeMessage, err = GetDefaultMergeMessage(db.DefaultContext, gitRepo, pr, "")
+	mergeMessage, _, err = GetDefaultMergeMessage(db.DefaultContext, gitRepo, pr, "")
 	assert.NoError(t, err)
 
 	assert.Equal(t, "Merge pull request 'issue3' (#3) from user2/repo2:branch2 into master", mergeMessage)
diff --git a/templates/repo/issue/view_content/pull.tmpl b/templates/repo/issue/view_content/pull.tmpl
index d68f3e5414..665f782053 100644
--- a/templates/repo/issue/view_content/pull.tmpl
+++ b/templates/repo/issue/view_content/pull.tmpl
@@ -343,7 +343,8 @@
 							(() => {
 								const defaultMergeTitle = {{.DefaultMergeMessage}};
 								const defaultSquashMergeTitle = {{.DefaultSquashMergeMessage}};
-								const defaultMergeMessage = 'Reviewed-on: ' + {{$.Issue.HTMLURL}} + '\n' + {{$approvers}};
+								const defaultMergeMessage = {{if .DefaultMergeBody}}{{.DefaultMergeBody}}{{else}}'Reviewed-on: ' + {{$.Issue.HTMLURL}} + '\n' + {{$approvers}}{{end}};
+								const defaultSquashMergeMessage = {{if .DefaultSquashMergeBody}}{{.DefaultSquashMergeBody}}{{else}}'Reviewed-on: ' + {{$.Issue.HTMLURL}} + '\n' + {{$approvers}}{{end}};
 								const mergeForm = {
 									'baseLink': {{.Link}},
 									'textCancel': {{$.locale.Tr "cancel"}},
@@ -398,7 +399,7 @@
 										'allowed': {{$prUnit.PullRequestsConfig.AllowSquash}},
 										'textDoMerge': {{$.locale.Tr "repo.pulls.squash_merge_pull_request"}},
 										'mergeTitleFieldText': defaultSquashMergeTitle,
-										'mergeMessageFieldText': {{.GetCommitMessages}} + defaultMergeMessage,
+										'mergeMessageFieldText': {{.GetCommitMessages}} + defaultSquashMergeMessage,
 										'hideAutoMerge': generalHideAutoMerge,
 									},
 									{