diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml
index 050a9e2d06..d50c89838d 100644
--- a/models/fixtures/repository.yml
+++ b/models/fixtures/repository.yml
@@ -37,7 +37,7 @@
   lower_name: repo2
   name: repo2
   default_branch: master
-  num_watches: 0
+  num_watches: 1
   num_stars: 1
   num_forks: 0
   num_issues: 2
diff --git a/models/fixtures/watch.yml b/models/fixtures/watch.yml
index c29f6bb65a..c6c9726cc8 100644
--- a/models/fixtures/watch.yml
+++ b/models/fixtures/watch.yml
@@ -26,4 +26,10 @@
   id: 5
   user_id: 11
   repo_id: 1
-  mode: 3 # auto 
+  mode: 3 # auto
+
+-
+  id: 6
+  user_id: 4
+  repo_id: 2
+  mode: 1 # normal
diff --git a/models/repo/user_repo.go b/models/repo/user_repo.go
index dd2ef62201..5d6e24e2a5 100644
--- a/models/repo/user_repo.go
+++ b/models/repo/user_repo.go
@@ -177,3 +177,16 @@ func GetIssuePostersWithSearch(ctx context.Context, repo *Repository, isPull boo
 		Limit(30).
 		Find(&users)
 }
+
+// GetWatchedRepoIDsOwnedBy returns the repos owned by a particular user watched by a particular user
+func GetWatchedRepoIDsOwnedBy(ctx context.Context, userID, ownedByUserID int64) ([]int64, error) {
+	repoIDs := make([]int64, 0, 10)
+	err := db.GetEngine(ctx).
+		Table("repository").
+		Select("`repository`.id").
+		Join("LEFT", "watch", "`repository`.id=`watch`.repo_id").
+		Where("`watch`.user_id=?", userID).
+		And("`watch`.mode<>?", WatchModeDont).
+		And("`repository`.owner_id=?", ownedByUserID).Find(&repoIDs)
+	return repoIDs, err
+}
diff --git a/models/repo/user_repo_test.go b/models/repo/user_repo_test.go
index 7816b0262a..ad794beb9b 100644
--- a/models/repo/user_repo_test.go
+++ b/models/repo/user_repo_test.go
@@ -9,6 +9,7 @@ import (
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
 
 	"github.com/stretchr/testify/assert"
 )
@@ -71,3 +72,15 @@ func TestRepoGetReviewers(t *testing.T) {
 	assert.NoError(t, err)
 	assert.Len(t, reviewers, 1)
 }
+
+func GetWatchedRepoIDsOwnedBy(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 9})
+	user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+
+	repoIDs, err := repo_model.GetWatchedRepoIDsOwnedBy(db.DefaultContext, user1.ID, user2.ID)
+	assert.NoError(t, err)
+	assert.Len(t, repoIDs, 1)
+	assert.EqualValues(t, 1, repoIDs[0])
+}
diff --git a/models/repo/watch.go b/models/repo/watch.go
index 6ff3a3f7b3..53b35c0e51 100644
--- a/models/repo/watch.go
+++ b/models/repo/watch.go
@@ -201,3 +201,9 @@ func WatchIfAuto(ctx context.Context, userID, repoID int64, isWrite bool) error
 	}
 	return watchRepoMode(ctx, watch, WatchModeAuto)
 }
+
+// UnwatchRepos will unwatch the user from all given repositories.
+func UnwatchRepos(ctx context.Context, userID int64, repoIDs []int64) error {
+	_, err := db.GetEngine(ctx).Where("user_id=?", userID).In("repo_id", repoIDs).Delete(&Watch{})
+	return err
+}
diff --git a/models/repo/watch_test.go b/models/repo/watch_test.go
index b6ae2a0ef5..02d0d3b0dd 100644
--- a/models/repo/watch_test.go
+++ b/models/repo/watch_test.go
@@ -155,3 +155,16 @@ func TestWatchRepoMode(t *testing.T) {
 	assert.NoError(t, repo_model.WatchRepoMode(12, 1, repo_model.WatchModeNone))
 	unittest.AssertCount(t, &repo_model.Watch{UserID: 12, RepoID: 1}, 0)
 }
+
+func TestUnwatchRepos(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{UserID: 4, RepoID: 1})
+	unittest.AssertExistsAndLoadBean(t, &repo_model.Watch{UserID: 4, RepoID: 2})
+
+	err := repo_model.UnwatchRepos(db.DefaultContext, 4, []int64{1, 2})
+	assert.NoError(t, err)
+
+	unittest.AssertNotExistsBean(t, &repo_model.Watch{UserID: 4, RepoID: 1})
+	unittest.AssertNotExistsBean(t, &repo_model.Watch{UserID: 4, RepoID: 2})
+}
diff --git a/models/user/block.go b/models/user/block.go
index 64dd93ed38..838bc7431e 100644
--- a/models/user/block.go
+++ b/models/user/block.go
@@ -53,10 +53,12 @@ func UnblockUser(ctx context.Context, userID, blockID int64) error {
 }
 
 // ListBlockedUsers returns the users that the user has blocked.
+// The created_unix field of the user struct is overridden by the creation_unix
+// field of blockeduser.
 func ListBlockedUsers(ctx context.Context, userID int64) ([]*User, error) {
 	users := make([]*User, 0, 8)
 	err := db.GetEngine(ctx).
-		Select("`user`.*").
+		Select("`forgejo_blocked_user`.created_unix, `user`.*").
 		Join("INNER", "forgejo_blocked_user", "`user`.id=`forgejo_blocked_user`.block_id").
 		Where("`forgejo_blocked_user`.user_id=?", userID).
 		Find(&users)
diff --git a/models/user/follow.go b/models/user/follow.go
index 936efbc164..5b3ff489ca 100644
--- a/models/user/follow.go
+++ b/models/user/follow.go
@@ -24,16 +24,25 @@ func init() {
 
 // IsFollowing returns true if user is following followID.
 func IsFollowing(userID, followID int64) bool {
-	has, _ := db.GetEngine(db.DefaultContext).Get(&Follow{UserID: userID, FollowID: followID})
+	return IsFollowingCtx(db.DefaultContext, userID, followID)
+}
+
+// IsFollowingCtx returns true if user is following followID.
+func IsFollowingCtx(ctx context.Context, userID, followID int64) bool {
+	has, _ := db.GetEngine(ctx).Get(&Follow{UserID: userID, FollowID: followID})
 	return has
 }
 
 // FollowUser marks someone be another's follower.
 func FollowUser(ctx context.Context, userID, followID int64) (err error) {
-	if userID == followID || IsFollowing(userID, followID) {
+	if userID == followID || IsFollowingCtx(ctx, userID, followID) {
 		return nil
 	}
 
+	if IsBlocked(ctx, userID, followID) || IsBlocked(ctx, followID, userID) {
+		return ErrBlockedByUser
+	}
+
 	ctx, committer, err := db.TxContext(ctx)
 	if err != nil {
 		return err
@@ -56,7 +65,7 @@ func FollowUser(ctx context.Context, userID, followID int64) (err error) {
 
 // UnfollowUser unmarks someone as another's follower.
 func UnfollowUser(ctx context.Context, userID, followID int64) (err error) {
-	if userID == followID || !IsFollowing(userID, followID) {
+	if userID == followID || !IsFollowingCtx(ctx, userID, followID) {
 		return nil
 	}
 
diff --git a/models/user/user_test.go b/models/user/user_test.go
index d5f0e80510..e21e9ad52e 100644
--- a/models/user/user_test.go
+++ b/models/user/user_test.go
@@ -457,6 +457,12 @@ func TestFollowUser(t *testing.T) {
 
 	assert.NoError(t, user_model.FollowUser(db.DefaultContext, 2, 2))
 
+	// Blocked user.
+	assert.ErrorIs(t, user_model.ErrBlockedByUser, user_model.FollowUser(db.DefaultContext, 1, 4))
+	assert.ErrorIs(t, user_model.ErrBlockedByUser, user_model.FollowUser(db.DefaultContext, 4, 1))
+	unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: 1, FollowID: 4})
+	unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: 4, FollowID: 1})
+
 	unittest.CheckConsistencyFor(t, &user_model.User{})
 }
 
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index b346971397..d9592b9663 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -601,6 +601,7 @@ block_user = Block User
 block_user.detail = Please understand that if you block this user, other actions will be taken. Such as:
 block_user.detail_1 = You are being unfollowed from this user.
 block_user.detail_2 = This user cannot interact with your repositories, created issues and comments.
+follow_blocked_user = You cannot follow this user because you have blocked this user or this user has blocked you.
 
 form.name_reserved = The username "%s" is reserved.
 form.name_pattern_not_allowed = The pattern "%s" is not allowed in a username.
@@ -892,6 +893,7 @@ hooks.desc = Add webhooks which will be triggered for <strong>all repositories</
 
 orgs_none = You are not a member of any organizations.
 repos_none = You do not own any repositories
+blocked_users_none = You haven't blocked any users.
 
 delete_account = Delete Your Account
 delete_prompt = This operation will permanently delete your user account. It <strong>CANNOT</strong> be undone.
@@ -914,6 +916,10 @@ visibility.limited_tooltip = Visible to authenticated users only
 visibility.private = Private
 visibility.private_tooltip = Visible only to organization members
 
+blocked_since = Blocked since %s
+user_unblock_success = The user has been unblocked successfully.
+user_block_success = The user has been blocked successfully.
+
 [repo]
 new_repo_helper = A repository contains all project files, including revision history.  Already have it elsewhere? <a href="%s">Migrate repository.</a>
 owner = Owner
@@ -2524,6 +2530,7 @@ team_access_desc = Repository access
 team_permission_desc = Permission
 team_unit_desc = Allow Access to Repository Sections
 team_unit_disabled = (Disabled)
+follow_blocked_user = You cannot follow this organisation because this organisation has blocked you.
 
 form.name_reserved = The organization name "%s" is reserved.
 form.name_pattern_not_allowed = The pattern "%s" is not allowed in an organization name.
diff --git a/routers/api/v1/user/follower.go b/routers/api/v1/user/follower.go
index 364507711d..bc1e6724f7 100644
--- a/routers/api/v1/user/follower.go
+++ b/routers/api/v1/user/follower.go
@@ -5,6 +5,7 @@
 package user
 
 import (
+	"errors"
 	"net/http"
 
 	user_model "code.gitea.io/gitea/models/user"
@@ -217,8 +218,14 @@ func Follow(ctx *context.APIContext) {
 	// responses:
 	//   "204":
 	//     "$ref": "#/responses/empty"
+	//   "403":
+	//     "$ref": "#/responses/forbidden"
 
 	if err := user_model.FollowUser(ctx, ctx.Doer.ID, ctx.ContextUser.ID); err != nil {
+		if errors.Is(err, user_model.ErrBlockedByUser) {
+			ctx.Error(http.StatusForbidden, "BlockedByUser", err)
+			return
+		}
 		ctx.Error(http.StatusInternalServerError, "FollowUser", err)
 		return
 	}
diff --git a/routers/web/org/setting/blocked_users.go b/routers/web/org/setting/blocked_users.go
new file mode 100644
index 0000000000..eae6f81fa0
--- /dev/null
+++ b/routers/web/org/setting/blocked_users.go
@@ -0,0 +1,61 @@
+// Copyright 2023 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package setting
+
+import (
+	"net/http"
+	"strings"
+
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/context"
+	"code.gitea.io/gitea/routers/utils"
+	user_service "code.gitea.io/gitea/services/user"
+)
+
+const tplBlockedUsers = "org/settings/blocked_users"
+
+// BlockedUsers renders the blocked users page.
+func BlockedUsers(ctx *context.Context) {
+	ctx.Data["Title"] = ctx.Tr("settings.blocked_users")
+	ctx.Data["PageIsSettingsBlockedUsers"] = true
+
+	blockedUsers, err := user_model.ListBlockedUsers(ctx, ctx.Org.Organization.ID)
+	if err != nil {
+		ctx.ServerError("ListBlockedUsers", err)
+		return
+	}
+
+	ctx.Data["BlockedUsers"] = blockedUsers
+
+	ctx.HTML(http.StatusOK, tplBlockedUsers)
+}
+
+// BlockedUsersBlock blocks a particular user from the organization.
+func BlockedUsersBlock(ctx *context.Context) {
+	uname := utils.RemoveUsernameParameterSuffix(strings.ToLower(ctx.FormString("uname")))
+	u, err := user_model.GetUserByName(ctx, uname)
+	if err != nil {
+		ctx.ServerError("GetUserByName", err)
+		return
+	}
+
+	if err := user_service.BlockUser(ctx, ctx.Org.Organization.ID, u.ID); err != nil {
+		ctx.ServerError("BlockUser", err)
+		return
+	}
+
+	ctx.Flash.Success(ctx.Tr("settings.user_block_success"))
+	ctx.Redirect(ctx.Org.OrgLink + "/settings/blocked_users")
+}
+
+// BlockedUsersUnblock unblocks a particular user from the organization.
+func BlockedUsersUnblock(ctx *context.Context) {
+	if err := user_model.UnblockUser(ctx, ctx.Org.Organization.ID, ctx.FormInt64("user_id")); err != nil {
+		ctx.ServerError("BlockUser", err)
+		return
+	}
+
+	ctx.Flash.Success(ctx.Tr("settings.user_unblock_success"))
+	ctx.Redirect(ctx.Org.OrgLink + "/settings/blocked_users")
+}
diff --git a/routers/web/user/profile.go b/routers/web/user/profile.go
index d91bf23f91..d103ed4ae2 100644
--- a/routers/web/user/profile.go
+++ b/routers/web/user/profile.go
@@ -5,6 +5,7 @@
 package user
 
 import (
+	"errors"
 	"fmt"
 	"net/http"
 	"strings"
@@ -369,8 +370,16 @@ func Action(ctx *context.Context) {
 	}
 
 	if err != nil {
-		ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.FormString("action")), err)
-		return
+		if !errors.Is(err, user_model.ErrBlockedByUser) {
+			ctx.ServerError(fmt.Sprintf("Action (%s)", ctx.FormString("action")), err)
+			return
+		}
+
+		if ctx.ContextUser.IsOrganization() {
+			ctx.Flash.Error(ctx.Tr("org.follow_blocked_user"))
+		} else {
+			ctx.Flash.Error(ctx.Tr("user.follow_blocked_user"))
+		}
 	}
 
 	if redirectViaJSON {
diff --git a/routers/web/user/setting/blocked_users.go b/routers/web/user/setting/blocked_users.go
index ea6ccf74d9..134becf969 100644
--- a/routers/web/user/setting/blocked_users.go
+++ b/routers/web/user/setting/blocked_users.go
@@ -32,3 +32,14 @@ func BlockedUsers(ctx *context.Context) {
 	ctx.Data["BlockedUsers"] = blockedUsers
 	ctx.HTML(http.StatusOK, tplSettingsBlockedUsers)
 }
+
+// UnblockUser unblocks a particular user for the doer.
+func UnblockUser(ctx *context.Context) {
+	if err := user_model.UnblockUser(ctx, ctx.Doer.ID, ctx.FormInt64("user_id")); err != nil {
+		ctx.ServerError("UnblockUser", err)
+		return
+	}
+
+	ctx.Flash.Success(ctx.Tr("settings.user_unblock_success"))
+	ctx.Redirect(setting.AppSubURL + "/user/settings/blocked_users")
+}
diff --git a/routers/web/web.go b/routers/web/web.go
index 46f4080d7d..8e64be56c3 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -517,7 +517,10 @@ func registerRoutes(m *web.Route) {
 			addWebhookEditRoutes()
 		}, webhooksEnabled)
 
-		m.Get("/blocked_users", user_setting.BlockedUsers)
+		m.Group("/blocked_users", func() {
+			m.Get("", user_setting.BlockedUsers)
+			m.Post("/unblock", user_setting.UnblockUser)
+		})
 	}, reqSignIn, ctxDataSet("PageIsUserSettings", true, "AllThemes", setting.UI.Themes, "EnablePackages", setting.Packages.Enabled))
 
 	m.Group("/user", func() {
@@ -770,6 +773,12 @@ func registerRoutes(m *web.Route) {
 					addSettingsSecretsRoutes()
 				}, actions.MustEnableActions)
 
+				m.Group("/blocked_users", func() {
+					m.Get("", org_setting.BlockedUsers)
+					m.Post("/block", org_setting.BlockedUsersBlock)
+					m.Post("/unblock", org_setting.BlockedUsersUnblock)
+				})
+
 				m.RouteMethods("/delete", "GET,POST", org.SettingsDelete)
 
 				m.Group("/packages", func() {
diff --git a/services/user/block.go b/services/user/block.go
index eff3242784..05f9a376a7 100644
--- a/services/user/block.go
+++ b/services/user/block.go
@@ -6,6 +6,7 @@ import (
 	"context"
 
 	"code.gitea.io/gitea/models/db"
+	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 )
 
@@ -30,11 +31,28 @@ func BlockUser(ctx context.Context, userID, blockID int64) error {
 		return err
 	}
 
-	// Unfollow the user from block's perspective.
+	// Unfollow the user from the block's perspective.
 	err = user_model.UnfollowUser(ctx, blockID, userID)
 	if err != nil {
 		return err
 	}
 
+	// Unfollow the user from the doer's perspective.
+	err = user_model.UnfollowUser(ctx, userID, blockID)
+	if err != nil {
+		return err
+	}
+
+	// Blocked user unwatch all repository owned by the doer.
+	repoIDs, err := repo_model.GetWatchedRepoIDsOwnedBy(ctx, blockID, userID)
+	if err != nil {
+		return err
+	}
+
+	err = repo_model.UnwatchRepos(ctx, blockID, repoIDs)
+	if err != nil {
+		return err
+	}
+
 	return committer.Commit()
 }
diff --git a/services/user/block_test.go b/services/user/block_test.go
new file mode 100644
index 0000000000..8a0a3c4739
--- /dev/null
+++ b/services/user/block_test.go
@@ -0,0 +1,41 @@
+// Copyright 2023 The Forgejo Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package user
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/db"
+	repo_model "code.gitea.io/gitea/models/repo"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+
+	"github.com/stretchr/testify/assert"
+)
+
+// TestBlockUser will ensure that when you block a user, certain actions have
+// been taken, like unfollowing each other etc.
+func TestBlockUser(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
+	blockedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+
+	// Follow each other.
+	assert.NoError(t, user_model.FollowUser(db.DefaultContext, doer.ID, blockedUser.ID))
+	assert.NoError(t, user_model.FollowUser(db.DefaultContext, blockedUser.ID, doer.ID))
+
+	// Blocked user watch repository of doer.
+	repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: doer.ID})
+	assert.NoError(t, repo_model.WatchRepo(db.DefaultContext, blockedUser.ID, repo.ID, true))
+
+	assert.NoError(t, BlockUser(db.DefaultContext, doer.ID, blockedUser.ID))
+
+	// Ensure they aren't following each other anymore.
+	assert.False(t, user_model.IsFollowing(doer.ID, blockedUser.ID))
+	assert.False(t, user_model.IsFollowing(blockedUser.ID, doer.ID))
+
+	// Ensure blocked user isn't following doer's repository.
+	assert.False(t, repo_model.IsWatching(blockedUser.ID, repo.ID))
+}
diff --git a/templates/org/home.tmpl b/templates/org/home.tmpl
index d540f80352..2e53385f36 100644
--- a/templates/org/home.tmpl
+++ b/templates/org/home.tmpl
@@ -1,5 +1,10 @@
 {{template "base/head" .}}
 <div role="main" aria-label="{{.Title}}" class="page-content organization profile">
+	{{if .Flash}}
+		<div class="ui container gt-mb-5">
+			{{template "base/alert" .}}
+		</div>
+	{{end}}
 	<div class="ui container gt-df">
 		{{avatar $.Context .Org 140 "org-avatar"}}
 		<div id="org-info">
diff --git a/templates/org/settings/blocked_users.tmpl b/templates/org/settings/blocked_users.tmpl
new file mode 100644
index 0000000000..e551ee5d67
--- /dev/null
+++ b/templates/org/settings/blocked_users.tmpl
@@ -0,0 +1,40 @@
+{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings blocked-users")}}
+<div class="org-setting-content">
+	<div class="ui attached segment">
+		<form class="ui form ignore-dirty" id="block-user-form" action="{{$.Link}}/block" method="post">
+			{{.CsrfTokenHtml}}
+			<input type="hidden" name="uid" value="">
+			<div class="inline field ui left">
+				<div id="search-user-box" class="ui search">
+					<div class="ui input">
+						<input class="prompt" name="uname" placeholder="{{.locale.Tr "repo.settings.search_user_placeholder"}}" autocomplete="off" required>
+					</div>
+				</div>
+			</div>
+			<button type="submit" class="ui red button">{{.locale.Tr "user.block"}}</button>
+		</form>
+	</div>
+	<div class="ui bottom attached table segment blocked-users">
+		{{range .BlockedUsers}}
+			<div class="item gt-df gt-ac gt-fw">
+				{{avatar $.Context . 48 "gt-mr-3 gt-mb-0"}}
+					<div class="gt-df gt-fc">
+						<a href="{{.HomeLink}}">{{.Name}}</a>
+						<i class="gt-mt-2">{{$.locale.Tr "settings.blocked_since" (DateTime "short" .CreatedUnix) | Safe}}</i>
+					</div>
+					<div class="gt-ml-auto content">
+						<form action="{{$.Link}}/unblock" method="post">
+							{{$.CsrfTokenHtml}}
+							<input type="hidden" name="user_id" value="{{.ID}}">
+							<button class="ui red button">{{$.locale.Tr "user.unblock"}}</button>
+						</form>
+					</div>
+				</div>
+			{{else}}
+				<div class="item">
+					<span class="text grey italic">{{$.locale.Tr "settings.blocked_users_none"}}</span>
+				</div>
+			{{end}}
+		</div>
+</div>
+{{template "org/settings/layout_footer" .}}
diff --git a/templates/org/settings/navbar.tmpl b/templates/org/settings/navbar.tmpl
index 6bea9f5f60..20c3279b7b 100644
--- a/templates/org/settings/navbar.tmpl
+++ b/templates/org/settings/navbar.tmpl
@@ -35,6 +35,9 @@
 			</div>
 		</details>
 		{{end}}
+			<a class="{{if .PageIsSettingsBlockedUsers}}active {{end}}item" href="{{.OrgLink}}/settings/blocked_users">
+			{{.locale.Tr "settings.blocked_users"}}
+		</a>
 		<a class="{{if .PageIsSettingsDelete}}active {{end}}item" href="{{.OrgLink}}/settings/delete">
 			{{.locale.Tr "org.settings.delete"}}
 		</a>
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index e7d1cc1fe5..d1f21b3cf9 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -13966,6 +13966,9 @@
         "responses": {
           "204": {
             "$ref": "#/responses/empty"
+          },
+          "403": {
+            "$ref": "#/responses/forbidden"
           }
         }
       },
diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl
index 58aab7167c..ddce4bbdab 100644
--- a/templates/user/profile.tmpl
+++ b/templates/user/profile.tmpl
@@ -1,6 +1,7 @@
 {{template "base/head" .}}
 <div role="main" aria-label="{{.Title}}" class="page-content user profile">
 	<div class="ui container">
+		{{template "base/alert" .}}
 		<div class="ui stackable grid">
 			<div class="ui four wide column">
 				<div class="ui card">
diff --git a/templates/user/settings/blocked_users.tmpl b/templates/user/settings/blocked_users.tmpl
index fd0cb07883..dc90970ec2 100644
--- a/templates/user/settings/blocked_users.tmpl
+++ b/templates/user/settings/blocked_users.tmpl
@@ -6,8 +6,23 @@
 		<div class="ui attached segment">
 			<div class="ui blocked-user list gt-mt-0">
 				{{range .BlockedUsers}}
+					<div class="item gt-df gt-ac">
+						{{avatar $.Context . 28 "gt-mr-3"}}
+						<div class="gt-df gt-fc">
+							<a href="{{.HomeLink}}">{{.Name}}</a>
+							<i class="gt-mt-2">{{$.locale.Tr "settings.blocked_since" (DateTime "short" .CreatedUnix) | Safe}}</i>
+						</div>
+						<div class="gt-ml-auto content">
+							<form action="{{$.Link}}/unblock" method="post">
+								{{$.CsrfTokenHtml}}
+								<input type="hidden" name="user_id" value="{{.ID}}">
+								<button class="ui red button">{{$.locale.Tr "user.unblock"}}</button>
+							</form>
+						</div>
+					</div>
+				{{else}}
 					<div class="item">
-						{{avatar $.Context . 28 "gt-mr-3"}}<a href="{{.HomeLink}}">{{.Name}}</a>
+						<span class="text grey italic">{{$.locale.Tr "settings.blocked_users_none"}}</span>
 					</div>
 				{{end}}
 			</div>
diff --git a/tests/integration/api_user_follow_test.go b/tests/integration/api_user_follow_test.go
index 62717af90e..bf6560b103 100644
--- a/tests/integration/api_user_follow_test.go
+++ b/tests/integration/api_user_follow_test.go
@@ -19,7 +19,7 @@ func TestAPIFollow(t *testing.T) {
 	defer tests.PrepareTestEnv(t)()
 
 	user1 := "user4"
-	user2 := "user1"
+	user2 := "user10"
 
 	session1 := loginUser(t, user1)
 	token1 := getTokenForLoggedInUser(t, session1, auth_model.AccessTokenScopeReadUser)
diff --git a/tests/integration/block_test.go b/tests/integration/block_test.go
index 03a5b14712..b8001f4968 100644
--- a/tests/integration/block_test.go
+++ b/tests/integration/block_test.go
@@ -41,7 +41,7 @@ func BlockUser(t *testing.T, doer, blockedUser *user_model.User) {
 	var respBody redirect
 	DecodeJSON(t, resp, &respBody)
 	assert.EqualValues(t, "/"+blockedUser.Name, respBody.Redirect)
-	assert.EqualValues(t, true, unittest.BeanExists(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: doer.ID}))
+	assert.True(t, unittest.BeanExists(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: doer.ID}))
 }
 
 func TestBlockUser(t *testing.T) {
@@ -156,3 +156,57 @@ func TestBlockCommentReaction(t *testing.T) {
 
 	assert.EqualValues(t, true, respBody.Empty)
 }
+
+// TestBlockFollow ensures that the doer and blocked user cannot follow each other.
+func TestBlockFollow(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+	doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
+	blockedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+
+	BlockUser(t, doer, blockedUser)
+
+	// Doer cannot follow blocked user.
+	session := loginUser(t, doer.Name)
+	req := NewRequestWithValues(t, "POST", "/"+blockedUser.Name, map[string]string{
+		"_csrf":  GetCSRF(t, session, "/"+blockedUser.Name),
+		"action": "follow",
+	})
+	session.MakeRequest(t, req, http.StatusSeeOther)
+
+	unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: doer.ID, FollowID: blockedUser.ID})
+
+	// Blocked user cannot follow doer.
+	session = loginUser(t, blockedUser.Name)
+	req = NewRequestWithValues(t, "POST", "/"+doer.Name, map[string]string{
+		"_csrf":  GetCSRF(t, session, "/"+doer.Name),
+		"action": "follow",
+	})
+	session.MakeRequest(t, req, http.StatusSeeOther)
+
+	unittest.AssertNotExistsBean(t, &user_model.Follow{UserID: blockedUser.ID, FollowID: doer.ID})
+}
+
+// TestBlockUserFromOrganization ensures that an organisation can block and unblock an user.
+func TestBlockUserFromOrganization(t *testing.T) {
+	defer tests.PrepareTestEnv(t)()
+
+	doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 15})
+	blockedUser := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
+	org := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 17, Type: user_model.UserTypeOrganization})
+	unittest.AssertNotExistsBean(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: org.ID})
+
+	session := loginUser(t, doer.Name)
+	req := NewRequestWithValues(t, "POST", org.OrganisationLink()+"/settings/blocked_users/block", map[string]string{
+		"_csrf": GetCSRF(t, session, org.OrganisationLink()+"/settings/blocked_users"),
+		"uname": blockedUser.Name,
+	})
+	session.MakeRequest(t, req, http.StatusSeeOther)
+	assert.True(t, unittest.BeanExists(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: org.ID}))
+
+	req = NewRequestWithValues(t, "POST", org.OrganisationLink()+"/settings/blocked_users/unblock", map[string]string{
+		"_csrf":   GetCSRF(t, session, org.OrganisationLink()+"/settings/blocked_users"),
+		"user_id": strconv.FormatInt(blockedUser.ID, 10),
+	})
+	session.MakeRequest(t, req, http.StatusSeeOther)
+	unittest.AssertNotExistsBean(t, &user_model.BlockedUser{BlockID: blockedUser.ID, UserID: org.ID})
+}
diff --git a/web_src/css/org.css b/web_src/css/org.css
index 9711ed25ad..30322a2c99 100644
--- a/web_src/css/org.css
+++ b/web_src/css/org.css
@@ -191,30 +191,35 @@
 }
 
 .organization.teams .repositories .item,
-.organization.teams .members .item {
+.organization.teams .members .item,
+.organization.settings .blocked-users .item {
   padding: 10px 19px;
 }
 
 .organization.teams .repositories .item:not(:last-child),
-.organization.teams .members .item:not(:last-child) {
+.organization.teams .members .item:not(:last-child),
+.organization.settings .blocked-users .item:not(:last-child) {
   border-bottom: 1px solid var(--color-secondary);
 }
 
 .organization.teams .repositories .item .button,
-.organization.teams .members .item .button {
+.organization.teams .members .item .button,
+.organization.settings .blocked-users .item button {
   padding: 9px 10px;
   margin: 0;
 }
 
 .organization.teams #add-repo-form input,
 .organization.teams #repo-multiple-form input,
-.organization.teams #add-member-form input {
+.organization.teams #add-member-form input,
+.organization.settings #block-user-form input {
   margin-left: 0;
 }
 
 .organization.teams #add-repo-form .ui.button,
 .organization.teams #repo-multiple-form .ui.button,
-.organization.teams #add-member-form .ui.button {
+.organization.teams #add-member-form .ui.button,
+.organization.settings #block-user-form .ui.button {
   margin-left: 5px;
   margin-top: -3px;
 }