diff --git a/cmd/admin_auth_ldap.go b/cmd/admin_auth_ldap.go
index 06f9244d50..ec86b2c671 100644
--- a/cmd/admin_auth_ldap.go
+++ b/cmd/admin_auth_ldap.go
@@ -260,7 +260,6 @@ func parseLdapConfig(c *cli.Context, config *ldap.Source) error {
 	if c.IsSet("skip-local-2fa") {
 		config.SkipLocalTwoFA = c.Bool("skip-local-2fa")
 	}
-
 	return nil
 }
 
diff --git a/integrations/auth_ldap_test.go b/integrations/auth_ldap_test.go
index 6eb017017f..ef0fafc93d 100644
--- a/integrations/auth_ldap_test.go
+++ b/integrations/auth_ldap_test.go
@@ -11,6 +11,9 @@ import (
 	"strings"
 	"testing"
 
+	"code.gitea.io/gitea/models"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/services/auth"
 
 	"github.com/stretchr/testify/assert"
@@ -97,7 +100,13 @@ func getLDAPServerHost() string {
 	return host
 }
 
-func addAuthSourceLDAP(t *testing.T, sshKeyAttribute string) {
+func addAuthSourceLDAP(t *testing.T, sshKeyAttribute string, groupMapParams ...string) {
+	groupTeamMapRemoval := "off"
+	groupTeamMap := ""
+	if len(groupMapParams) == 2 {
+		groupTeamMapRemoval = groupMapParams[0]
+		groupTeamMap = groupMapParams[1]
+	}
 	session := loginUser(t, "user1")
 	csrf := GetCSRF(t, session, "/admin/auths/new")
 	req := NewRequestWithValues(t, "POST", "/admin/auths/new", map[string]string{
@@ -119,6 +128,12 @@ func addAuthSourceLDAP(t *testing.T, sshKeyAttribute string) {
 		"attribute_ssh_public_key": sshKeyAttribute,
 		"is_sync_enabled":          "on",
 		"is_active":                "on",
+		"groups_enabled":           "on",
+		"group_dn":                 "ou=people,dc=planetexpress,dc=com",
+		"group_member_uid":         "member",
+		"group_team_map":           groupTeamMap,
+		"group_team_map_removal":   groupTeamMapRemoval,
+		"user_uid":                 "DN",
 	})
 	session.MakeRequest(t, req, http.StatusFound)
 }
@@ -294,3 +309,105 @@ func TestLDAPUserSSHKeySync(t *testing.T) {
 		assert.ElementsMatch(t, u.SSHKeys, syncedKeys, "Unequal number of keys synchronized for user: %s", u.UserName)
 	}
 }
+
+func TestLDAPGroupTeamSyncAddMember(t *testing.T) {
+	if skipLDAPTests() {
+		t.Skip()
+		return
+	}
+	defer prepareTestEnv(t)()
+	addAuthSourceLDAP(t, "", "on", `{"cn=ship_crew,ou=people,dc=planetexpress,dc=com":{"org26": ["team11"]},"cn=admin_staff,ou=people,dc=planetexpress,dc=com": {"non-existent": ["non-existent"]}}`)
+	org, err := models.GetOrgByName("org26")
+	assert.NoError(t, err)
+	team, err := models.GetTeam(org.ID, "team11")
+	assert.NoError(t, err)
+	auth.SyncExternalUsers(context.Background(), true)
+	for _, gitLDAPUser := range gitLDAPUsers {
+		user := unittest.AssertExistsAndLoadBean(t, &user_model.User{
+			Name: gitLDAPUser.UserName,
+		}).(*user_model.User)
+		usersOrgs, err := models.FindOrgs(models.FindOrgOptions{
+			UserID:         user.ID,
+			IncludePrivate: true,
+		})
+		assert.NoError(t, err)
+		allOrgTeams, err := models.GetUserOrgTeams(org.ID, user.ID)
+		assert.NoError(t, err)
+		if user.Name == "fry" || user.Name == "leela" || user.Name == "bender" {
+			// assert members of LDAP group "cn=ship_crew" are added to mapped teams
+			assert.Equal(t, len(usersOrgs), 1, "User [%s] should be member of one organization", user.Name)
+			assert.Equal(t, usersOrgs[0].Name, "org26", "Membership should be added to the right organization")
+			isMember, err := models.IsTeamMember(usersOrgs[0].ID, team.ID, user.ID)
+			assert.NoError(t, err)
+			assert.True(t, isMember, "Membership should be added to the right team")
+			err = team.RemoveMember(user.ID)
+			assert.NoError(t, err)
+			err = usersOrgs[0].RemoveMember(user.ID)
+			assert.NoError(t, err)
+		} else {
+			// assert members of LDAP group "cn=admin_staff" keep initial team membership since mapped team does not exist
+			assert.Empty(t, usersOrgs, "User should be member of no organization")
+			isMember, err := models.IsTeamMember(org.ID, team.ID, user.ID)
+			assert.NoError(t, err)
+			assert.False(t, isMember, "User should no be added to this team")
+			assert.Empty(t, allOrgTeams, "User should not be added to any team")
+		}
+	}
+}
+
+func TestLDAPGroupTeamSyncRemoveMember(t *testing.T) {
+	if skipLDAPTests() {
+		t.Skip()
+		return
+	}
+	defer prepareTestEnv(t)()
+	addAuthSourceLDAP(t, "", "on", `{"cn=dispatch,ou=people,dc=planetexpress,dc=com": {"org26": ["team11"]}}`)
+	org, err := models.GetOrgByName("org26")
+	assert.NoError(t, err)
+	team, err := models.GetTeam(org.ID, "team11")
+	assert.NoError(t, err)
+	loginUserWithPassword(t, gitLDAPUsers[0].UserName, gitLDAPUsers[0].Password)
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{
+		Name: gitLDAPUsers[0].UserName,
+	}).(*user_model.User)
+	err = org.AddMember(user.ID)
+	assert.NoError(t, err)
+	err = team.AddMember(user.ID)
+	assert.NoError(t, err)
+	isMember, err := models.IsOrganizationMember(org.ID, user.ID)
+	assert.NoError(t, err)
+	assert.True(t, isMember, "User should be member of this organization")
+	isMember, err = models.IsTeamMember(org.ID, team.ID, user.ID)
+	assert.NoError(t, err)
+	assert.True(t, isMember, "User should be member of this team")
+	// assert team member "professor" gets removed from org26 team11
+	loginUserWithPassword(t, gitLDAPUsers[0].UserName, gitLDAPUsers[0].Password)
+	isMember, err = models.IsOrganizationMember(org.ID, user.ID)
+	assert.NoError(t, err)
+	assert.False(t, isMember, "User membership should have been removed from organization")
+	isMember, err = models.IsTeamMember(org.ID, team.ID, user.ID)
+	assert.NoError(t, err)
+	assert.False(t, isMember, "User membership should have been removed from team")
+}
+
+// Login should work even if Team Group Map contains a broken JSON
+func TestBrokenLDAPMapUserSignin(t *testing.T) {
+	if skipLDAPTests() {
+		t.Skip()
+		return
+	}
+	defer prepareTestEnv(t)()
+	addAuthSourceLDAP(t, "", "on", `{"NOT_A_VALID_JSON"["MISSING_DOUBLE_POINT"]}`)
+
+	u := gitLDAPUsers[0]
+
+	session := loginUserWithPassword(t, u.UserName, u.Password)
+	req := NewRequest(t, "GET", "/user/settings")
+	resp := session.MakeRequest(t, req, http.StatusOK)
+
+	htmlDoc := NewHTMLParser(t, resp.Body)
+
+	assert.Equal(t, u.UserName, htmlDoc.GetInputValueByName("name"))
+	assert.Equal(t, u.FullName, htmlDoc.GetInputValueByName("full_name"))
+	assert.Equal(t, u.Email, htmlDoc.Find(`label[for="email"]`).Siblings().First().Text())
+}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 1c43132478..722cade0eb 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -2581,11 +2581,13 @@ auths.filter = User Filter
 auths.admin_filter = Admin Filter
 auths.restricted_filter = Restricted Filter
 auths.restricted_filter_helper = Leave empty to not set any users as restricted. Use an asterisk ('*') to set all users that do not match Admin Filter as restricted.
-auths.verify_group_membership = Verify group membership in LDAP
+auths.verify_group_membership = Verify group membership in LDAP (leave the filter empty to skip)
 auths.group_search_base = Group Search Base DN
-auths.valid_groups_filter = Valid Groups Filter
 auths.group_attribute_list_users = Group Attribute Containing List Of Users
 auths.user_attribute_in_group = User Attribute Listed In Group
+auths.map_group_to_team = Map LDAP groups to Organization teams (leave the field empty to skip)
+auths.map_group_to_team_removal = Remove users from synchronized teams if user does not belong to corresponding LDAP group
+auths.enable_ldap_groups = Enable LDAP groups
 auths.ms_ad_sa = MS AD Search Attributes
 auths.smtp_auth = SMTP Authentication Type
 auths.smtphost = SMTP Host
diff --git a/routers/web/admin/auths.go b/routers/web/admin/auths.go
index 20f364739e..748f2e7a8b 100644
--- a/routers/web/admin/auths.go
+++ b/routers/web/admin/auths.go
@@ -145,6 +145,8 @@ func parseLDAPConfig(form forms.AuthenticationForm) *ldap.Source {
 		GroupDN:               form.GroupDN,
 		GroupFilter:           form.GroupFilter,
 		GroupMemberUID:        form.GroupMemberUID,
+		GroupTeamMap:          form.GroupTeamMap,
+		GroupTeamMapRemoval:   form.GroupTeamMapRemoval,
 		UserUID:               form.UserUID,
 		AdminFilter:           form.AdminFilter,
 		RestrictedFilter:      form.RestrictedFilter,
diff --git a/services/auth/source/ldap/README.md b/services/auth/source/ldap/README.md
index 3a839fa314..59fc5cabad 100644
--- a/services/auth/source/ldap/README.md
+++ b/services/auth/source/ldap/README.md
@@ -120,3 +120,11 @@ share the following fields:
 * Group Attribute for User (optional)
   * Which group LDAP attribute contains an array above user attribute names.
   * Example: memberUid
+
+* Team group map (optional)
+  * Automatically add users to Organization teams, depending on LDAP group memberships.
+  * Note: this function only adds users to teams, it never removes users.
+  * Example: {"cn=MyGroup,cn=groups,dc=example,dc=org": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2", ...], ...}, ...}
+
+* Team group map removal (optional)
+  * If set to true, users will be removed from teams if they are not members of the corresponding group.
diff --git a/services/auth/source/ldap/source.go b/services/auth/source/ldap/source.go
index fc778b0114..ad97e2dd49 100644
--- a/services/auth/source/ldap/source.go
+++ b/services/auth/source/ldap/source.go
@@ -52,6 +52,8 @@ type Source struct {
 	GroupDN               string // Group Search Base
 	GroupFilter           string // Group Name Filter
 	GroupMemberUID        string // Group Attribute containing array of UserUID
+	GroupTeamMap          string // Map LDAP groups to teams
+	GroupTeamMapRemoval   bool   // Remove user from teams which are synchronized and user is not a member of the corresponding LDAP group
 	UserUID               string // User Attribute listed in Group
 	SkipLocalTwoFA        bool   `json:",omitempty"` // Skip Local 2fa for users authenticated with this source
 
diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go
index 52971bb87e..e804e32e84 100644
--- a/services/auth/source/ldap/source_authenticate.go
+++ b/services/auth/source/ldap/source_authenticate.go
@@ -8,6 +8,7 @@ import (
 	"fmt"
 	"strings"
 
+	"code.gitea.io/gitea/models"
 	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/auth"
 	"code.gitea.io/gitea/models/db"
@@ -59,10 +60,14 @@ func (source *Source) Authenticate(user *user_model.User, userName, password str
 	}
 
 	if user != nil {
+		if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) {
+			orgCache := make(map[string]*models.Organization)
+			teamCache := make(map[string]*models.Team)
+			source.SyncLdapGroupsToTeams(user, sr.LdapTeamAdd, sr.LdapTeamRemove, orgCache, teamCache)
+		}
 		if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(user, source.authSource, sr.SSHPublicKey) {
 			return user, asymkey_model.RewriteAllPublicKeys()
 		}
-
 		return user, nil
 	}
 
@@ -98,10 +103,14 @@ func (source *Source) Authenticate(user *user_model.User, userName, password str
 	if isAttributeSSHPublicKeySet && asymkey_model.AddPublicKeysBySource(user, source.authSource, sr.SSHPublicKey) {
 		err = asymkey_model.RewriteAllPublicKeys()
 	}
-
 	if err == nil && len(source.AttributeAvatar) > 0 {
 		_ = user_service.UploadAvatar(user, sr.Avatar)
 	}
+	if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) {
+		orgCache := make(map[string]*models.Organization)
+		teamCache := make(map[string]*models.Team)
+		source.SyncLdapGroupsToTeams(user, sr.LdapTeamAdd, sr.LdapTeamRemove, orgCache, teamCache)
+	}
 
 	return user, err
 }
diff --git a/services/auth/source/ldap/source_group_sync.go b/services/auth/source/ldap/source_group_sync.go
new file mode 100644
index 0000000000..7c62af705e
--- /dev/null
+++ b/services/auth/source/ldap/source_group_sync.go
@@ -0,0 +1,100 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package ldap
+
+import (
+	"code.gitea.io/gitea/models"
+	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/log"
+)
+
+// SyncLdapGroupsToTeams maps LDAP groups to organization and team memberships
+func (source *Source) SyncLdapGroupsToTeams(user *user_model.User, ldapTeamAdd, ldapTeamRemove map[string][]string, orgCache map[string]*models.Organization, teamCache map[string]*models.Team) {
+	var err error
+	if source.GroupsEnabled && source.GroupTeamMapRemoval {
+		// when the user is not a member of configs LDAP group, remove mapped organizations/teams memberships
+		removeMappedMemberships(user, ldapTeamRemove, orgCache, teamCache)
+	}
+	for orgName, teamNames := range ldapTeamAdd {
+		org, ok := orgCache[orgName]
+		if !ok {
+			org, err = models.GetOrgByName(orgName)
+			if err != nil {
+				// organization must be created before LDAP group sync
+				log.Warn("LDAP group sync: Could not find organisation %s: %v", orgName, err)
+				continue
+			}
+			orgCache[orgName] = org
+		}
+		if isMember, err := models.IsOrganizationMember(org.ID, user.ID); !isMember && err == nil {
+			log.Trace("LDAP group sync: adding user [%s] to organization [%s]", user.Name, org.Name)
+			err = org.AddMember(user.ID)
+			if err != nil {
+				log.Error("LDAP group sync: Could not add user to organization: %v", err)
+				continue
+			}
+		}
+		for _, teamName := range teamNames {
+			team, ok := teamCache[orgName+teamName]
+			if !ok {
+				team, err = org.GetTeam(teamName)
+				if err != nil {
+					// team must be created before LDAP group sync
+					log.Warn("LDAP group sync: Could not find team %s: %v", teamName, err)
+					continue
+				}
+				teamCache[orgName+teamName] = team
+			}
+			if isMember, err := models.IsTeamMember(org.ID, team.ID, user.ID); !isMember && err == nil {
+				log.Trace("LDAP group sync: adding user [%s] to team [%s]", user.Name, org.Name)
+			} else {
+				continue
+			}
+			err := team.AddMember(user.ID)
+			if err != nil {
+				log.Error("LDAP group sync: Could not add user to team: %v", err)
+			}
+		}
+	}
+}
+
+// remove membership to organizations/teams if user is not member of corresponding LDAP group
+// e.g. lets assume user is member of LDAP group "x", but LDAP group team map contains LDAP groups "x" and "y"
+// then users membership gets removed for all organizations/teams mapped by LDAP group "y"
+func removeMappedMemberships(user *user_model.User, ldapTeamRemove map[string][]string, orgCache map[string]*models.Organization, teamCache map[string]*models.Team) {
+	var err error
+	for orgName, teamNames := range ldapTeamRemove {
+		org, ok := orgCache[orgName]
+		if !ok {
+			org, err = models.GetOrgByName(orgName)
+			if err != nil {
+				// organization must be created before LDAP group sync
+				log.Warn("LDAP group sync: Could not find organisation %s: %v", orgName, err)
+				continue
+			}
+			orgCache[orgName] = org
+		}
+		for _, teamName := range teamNames {
+			team, ok := teamCache[orgName+teamName]
+			if !ok {
+				team, err = org.GetTeam(teamName)
+				if err != nil {
+					// team must must be created before LDAP group sync
+					log.Warn("LDAP group sync: Could not find team %s: %v", teamName, err)
+					continue
+				}
+			}
+			if isMember, err := models.IsTeamMember(org.ID, team.ID, user.ID); isMember && err == nil {
+				log.Trace("LDAP group sync: removing user [%s] from team [%s]", user.Name, org.Name)
+			} else {
+				continue
+			}
+			err = team.RemoveMember(user.ID)
+			if err != nil {
+				log.Error("LDAP group sync: Could not remove user from team: %v", err)
+			}
+		}
+	}
+}
diff --git a/services/auth/source/ldap/source_search.go b/services/auth/source/ldap/source_search.go
index 1f1cca270d..f2b940cabe 100644
--- a/services/auth/source/ldap/source_search.go
+++ b/services/auth/source/ldap/source_search.go
@@ -12,22 +12,26 @@ import (
 	"strconv"
 	"strings"
 
+	"code.gitea.io/gitea/modules/json"
 	"code.gitea.io/gitea/modules/log"
+	"code.gitea.io/gitea/modules/util"
 
 	"github.com/go-ldap/ldap/v3"
 )
 
 // SearchResult : user data
 type SearchResult struct {
-	Username     string   // Username
-	Name         string   // Name
-	Surname      string   // Surname
-	Mail         string   // E-mail address
-	SSHPublicKey []string // SSH Public Key
-	IsAdmin      bool     // if user is administrator
-	IsRestricted bool     // if user is restricted
-	LowerName    string   // Lowername
-	Avatar       []byte
+	Username       string   // Username
+	Name           string   // Name
+	Surname        string   // Surname
+	Mail           string   // E-mail address
+	SSHPublicKey   []string // SSH Public Key
+	IsAdmin        bool     // if user is administrator
+	IsRestricted   bool     // if user is restricted
+	LowerName      string   // LowerName
+	Avatar         []byte
+	LdapTeamAdd    map[string][]string // organizations teams to add
+	LdapTeamRemove map[string][]string // organizations teams to remove
 }
 
 func (ls *Source) sanitizedUserQuery(username string) (string, bool) {
@@ -192,6 +196,71 @@ func checkRestricted(l *ldap.Conn, ls *Source, userDN string) bool {
 	return false
 }
 
+// List all group memberships of a user
+func (ls *Source) listLdapGroupMemberships(l *ldap.Conn, uid string) []string {
+	var ldapGroups []string
+	groupFilter := fmt.Sprintf("(%s=%s)", ls.GroupMemberUID, uid)
+	result, err := l.Search(ldap.NewSearchRequest(
+		ls.GroupDN,
+		ldap.ScopeWholeSubtree,
+		ldap.NeverDerefAliases,
+		0,
+		0,
+		false,
+		groupFilter,
+		[]string{},
+		nil,
+	))
+	if err != nil {
+		log.Error("Failed group search using filter[%s]: %v", groupFilter, err)
+		return ldapGroups
+	}
+
+	for _, entry := range result.Entries {
+		if entry.DN == "" {
+			log.Error("LDAP search was successful, but found no DN!")
+			continue
+		}
+		ldapGroups = append(ldapGroups, entry.DN)
+	}
+
+	return ldapGroups
+}
+
+// parse LDAP groups and return map of ldap groups to organizations teams
+func (ls *Source) mapLdapGroupsToTeams() map[string]map[string][]string {
+	ldapGroupsToTeams := make(map[string]map[string][]string)
+	err := json.Unmarshal([]byte(ls.GroupTeamMap), &ldapGroupsToTeams)
+	if err != nil {
+		log.Error("Failed to unmarshall LDAP teams map: %v", err)
+		return ldapGroupsToTeams
+	}
+	return ldapGroupsToTeams
+}
+
+// getMappedMemberships : returns the organizations and teams to modify the users membership
+func (ls *Source) getMappedMemberships(l *ldap.Conn, uid string) (map[string][]string, map[string][]string) {
+	// get all LDAP group memberships for user
+	usersLdapGroups := ls.listLdapGroupMemberships(l, uid)
+	// unmarshall LDAP group team map from configs
+	ldapGroupsToTeams := ls.mapLdapGroupsToTeams()
+	membershipsToAdd := map[string][]string{}
+	membershipsToRemove := map[string][]string{}
+	for group, memberships := range ldapGroupsToTeams {
+		isUserInGroup := util.IsStringInSlice(group, usersLdapGroups)
+		if isUserInGroup {
+			for org, teams := range memberships {
+				membershipsToAdd[org] = teams
+			}
+		} else if !isUserInGroup {
+			for org, teams := range memberships {
+				membershipsToRemove[org] = teams
+			}
+		}
+	}
+	return membershipsToAdd, membershipsToRemove
+}
+
 // SearchEntry : search an LDAP source if an entry (name, passwd) is valid and in the specific filter
 func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResult {
 	// See https://tools.ietf.org/search/rfc4513#section-5.1.2
@@ -308,9 +377,12 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul
 	surname := sr.Entries[0].GetAttributeValue(ls.AttributeSurname)
 	mail := sr.Entries[0].GetAttributeValue(ls.AttributeMail)
 	uid := sr.Entries[0].GetAttributeValue(ls.UserUID)
+	if ls.UserUID == "dn" || ls.UserUID == "DN" {
+		uid = sr.Entries[0].DN
+	}
 
 	// Check group membership
-	if ls.GroupsEnabled {
+	if ls.GroupsEnabled && ls.GroupFilter != "" {
 		groupFilter, ok := ls.sanitizedGroupFilter(ls.GroupFilter)
 		if !ok {
 			return nil
@@ -373,16 +445,24 @@ func (ls *Source) SearchEntry(name, passwd string, directBind bool) *SearchResul
 		Avatar = sr.Entries[0].GetRawAttributeValue(ls.AttributeAvatar)
 	}
 
+	teamsToAdd := make(map[string][]string)
+	teamsToRemove := make(map[string][]string)
+	if ls.GroupsEnabled && (ls.GroupTeamMap != "" || ls.GroupTeamMapRemoval) {
+		teamsToAdd, teamsToRemove = ls.getMappedMemberships(l, uid)
+	}
+
 	return &SearchResult{
-		LowerName:    strings.ToLower(username),
-		Username:     username,
-		Name:         firstname,
-		Surname:      surname,
-		Mail:         mail,
-		SSHPublicKey: sshPublicKey,
-		IsAdmin:      isAdmin,
-		IsRestricted: isRestricted,
-		Avatar:       Avatar,
+		LowerName:      strings.ToLower(username),
+		Username:       username,
+		Name:           firstname,
+		Surname:        surname,
+		Mail:           mail,
+		SSHPublicKey:   sshPublicKey,
+		IsAdmin:        isAdmin,
+		IsRestricted:   isRestricted,
+		Avatar:         Avatar,
+		LdapTeamAdd:    teamsToAdd,
+		LdapTeamRemove: teamsToRemove,
 	}
 }
 
@@ -417,7 +497,7 @@ func (ls *Source) SearchEntries() ([]*SearchResult, error) {
 	isAttributeSSHPublicKeySet := len(strings.TrimSpace(ls.AttributeSSHPublicKey)) > 0
 	isAtributeAvatarSet := len(strings.TrimSpace(ls.AttributeAvatar)) > 0
 
-	attribs := []string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail}
+	attribs := []string{ls.AttributeUsername, ls.AttributeName, ls.AttributeSurname, ls.AttributeMail, ls.UserUID}
 	if isAttributeSSHPublicKeySet {
 		attribs = append(attribs, ls.AttributeSSHPublicKey)
 	}
@@ -444,12 +524,23 @@ func (ls *Source) SearchEntries() ([]*SearchResult, error) {
 	result := make([]*SearchResult, len(sr.Entries))
 
 	for i, v := range sr.Entries {
+		teamsToAdd := make(map[string][]string)
+		teamsToRemove := make(map[string][]string)
+		if ls.GroupsEnabled && (ls.GroupTeamMap != "" || ls.GroupTeamMapRemoval) {
+			userAttributeListedInGroup := v.GetAttributeValue(ls.UserUID)
+			if ls.UserUID == "dn" || ls.UserUID == "DN" {
+				userAttributeListedInGroup = v.DN
+			}
+			teamsToAdd, teamsToRemove = ls.getMappedMemberships(l, userAttributeListedInGroup)
+		}
 		result[i] = &SearchResult{
-			Username: v.GetAttributeValue(ls.AttributeUsername),
-			Name:     v.GetAttributeValue(ls.AttributeName),
-			Surname:  v.GetAttributeValue(ls.AttributeSurname),
-			Mail:     v.GetAttributeValue(ls.AttributeMail),
-			IsAdmin:  checkAdmin(l, ls, v.DN),
+			Username:       v.GetAttributeValue(ls.AttributeUsername),
+			Name:           v.GetAttributeValue(ls.AttributeName),
+			Surname:        v.GetAttributeValue(ls.AttributeSurname),
+			Mail:           v.GetAttributeValue(ls.AttributeMail),
+			IsAdmin:        checkAdmin(l, ls, v.DN),
+			LdapTeamAdd:    teamsToAdd,
+			LdapTeamRemove: teamsToRemove,
 		}
 		if !result[i].IsAdmin {
 			result[i].IsRestricted = checkRestricted(l, ls, v.DN)
diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go
index 398d9ef798..74a62ce4e7 100644
--- a/services/auth/source/ldap/source_sync.go
+++ b/services/auth/source/ldap/source_sync.go
@@ -10,6 +10,7 @@ import (
 	"sort"
 	"strings"
 
+	"code.gitea.io/gitea/models"
 	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
@@ -61,6 +62,8 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
 	})
 
 	userPos := 0
+	orgCache := make(map[string]*models.Organization)
+	teamCache := make(map[string]*models.Team)
 
 	for _, su := range sr {
 		select {
@@ -166,6 +169,10 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
 				}
 			}
 		}
+		// Synchronize LDAP groups with organization and team memberships
+		if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) {
+			source.SyncLdapGroupsToTeams(usr, su.LdapTeamAdd, su.LdapTeamRemove, orgCache, teamCache)
+		}
 	}
 
 	// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed
diff --git a/services/forms/auth_form.go b/services/forms/auth_form.go
index d096292601..7e7c756752 100644
--- a/services/forms/auth_form.go
+++ b/services/forms/auth_form.go
@@ -79,6 +79,8 @@ type AuthenticationForm struct {
 	SSPIStripDomainNames          bool
 	SSPISeparatorReplacement      string `binding:"AlphaDashDot;MaxSize(5)"`
 	SSPIDefaultLanguage           string
+	GroupTeamMap                  string
+	GroupTeamMapRemoval           bool
 }
 
 // Validate validates fields
diff --git a/templates/admin/auth/edit.tmpl b/templates/admin/auth/edit.tmpl
index efa440ff33..31c87597f0 100644
--- a/templates/admin/auth/edit.tmpl
+++ b/templates/admin/auth/edit.tmpl
@@ -108,31 +108,43 @@
 						<label for="attribute_avatar">{{.i18n.Tr "admin.auths.attribute_avatar"}}</label>
 						<input id="attribute_avatar" name="attribute_avatar" value="{{$cfg.AttributeAvatar}}" placeholder="e.g. jpegPhoto">
 					</div>
+
+
+					<!-- ldap group begin -->
 					<div class="inline field">
 						<div class="ui checkbox">
-							<label for="groups_enabled"><strong>{{.i18n.Tr "admin.auths.verify_group_membership"}}</strong></label>
-							<input id="groups_enabled" name="groups_enabled" type="checkbox" {{if $cfg.GroupsEnabled}}checked{{end}}>
+							<label><strong>{{.i18n.Tr "admin.auths.enable_ldap_groups"}}</strong></label>
+							<input type="checkbox" name="groups_enabled" class="js-ldap-group-toggle" {{if $cfg.GroupsEnabled}}checked{{end}}>
 						</div>
 					</div>
-					<div id="groups_enabled_change">
+					<div id="ldap-group-options" class="ui segment secondary" {{if not $cfg.GroupsEnabled}}hidden{{end}}>
 						<div class="field">
-							<label for="group_dn">{{.i18n.Tr "admin.auths.group_search_base"}}</label>
-							<input id="group_dn" name="group_dn" value="{{$cfg.GroupDN}}" placeholder="e.g. ou=group,dc=mydomain,dc=com">
+							<label>{{.i18n.Tr "admin.auths.group_search_base"}}</label>
+							<input name="group_dn" value="{{$cfg.GroupDN}}" placeholder="e.g. ou=group,dc=mydomain,dc=com">
 						</div>
 						<div class="field">
-							<label for="group_filter">{{.i18n.Tr "admin.auths.valid_groups_filter"}}</label>
-							<input id="group_filter" name="group_filter" value="{{$cfg.GroupFilter}}" placeholder="e.g. (|(cn=gitea_users)(cn=admins))">
+							<label>{{.i18n.Tr "admin.auths.group_attribute_list_users"}}</label>
+							<input name="group_member_uid" value="{{$cfg.GroupMemberUID}}" placeholder="e.g. memberUid">
 						</div>
 						<div class="field">
-							<label for="group_member_uid">{{.i18n.Tr "admin.auths.group_attribute_list_users"}}</label>
-							<input id="group_member_uid" name="group_member_uid" value="{{$cfg.GroupMemberUID}}" placeholder="e.g. memberUid">
+							<label>{{.i18n.Tr "admin.auths.user_attribute_in_group"}}</label>
+							<input name="user_uid" value="{{$cfg.UserUID}}" placeholder="e.g. uid">
 						</div>
 						<div class="field">
-							<label for="user_uid">{{.i18n.Tr "admin.auths.user_attribute_in_group"}}</label>
-							<input id="user_uid" name="user_uid" value="{{$cfg.UserUID}}" placeholder="e.g. uid">
+							<label>{{.i18n.Tr "admin.auths.verify_group_membership"}}</label>
+							<input name="group_filter" value="{{$cfg.GroupFilter}}" placeholder="e.g. (|(cn=gitea_users)(cn=admins))">
+						</div>
+						<div class="field">
+							<label>{{.i18n.Tr "admin.auths.map_group_to_team"}}</label>
+							<input name="group_team_map" value="{{$cfg.GroupTeamMap}}" placeholder='e.g. {"cn=my-group,cn=groups,dc=example,dc=org": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}'>
+						</div>
+						<div class="ui checkbox">
+							<label>{{.i18n.Tr "admin.auths.map_group_to_team_removal"}}</label>
+							<input name="group_team_map_removal" type="checkbox" {{if $cfg.GroupTeamMapRemoval}}checked{{end}}>
 						</div>
-						<br/>
 					</div>
+					<!-- ldap group end -->
+
 					{{if .Source.IsLDAP}}
 						<div class="inline field">
 							<div class="ui checkbox">
diff --git a/templates/admin/auth/source/ldap.tmpl b/templates/admin/auth/source/ldap.tmpl
index 9ea0fdf8c0..afdfbadd65 100644
--- a/templates/admin/auth/source/ldap.tmpl
+++ b/templates/admin/auth/source/ldap.tmpl
@@ -79,31 +79,42 @@
 		<label for="attribute_avatar">{{.i18n.Tr "admin.auths.attribute_avatar"}}</label>
 		<input id="attribute_avatar" name="attribute_avatar" value="{{.attribute_avatar}}" placeholder="e.g. jpegPhoto">
 	</div>
+
+	<!-- ldap group begin -->
 	<div class="inline field">
 		<div class="ui checkbox">
-			<label for="groups_enabled"><strong>{{.i18n.Tr "admin.auths.verify_group_membership"}}</strong></label>
-			<input id="groups_enabled" name="groups_enabled" type="checkbox" {{if .groups_enabled}}checked{{end}}>
+			<label><strong>{{.i18n.Tr "admin.auths.enable_ldap_groups"}}</strong></label>
+			<input type="checkbox" name="groups_enabled" class="js-ldap-group-toggle" {{if .groups_enabled}}checked{{end}}>
 		</div>
 	</div>
-	<div id="groups_enabled_change">
+	<div id="ldap-group-options" class="ui segment secondary">
 		<div class="field">
-			<label for="group_dn">{{.i18n.Tr "admin.auths.group_search_base"}}</label>
-			<input id="group_dn" name="group_dn" value="{{.group_dn}}" placeholder="e.g. ou=group,dc=mydomain,dc=com">
+			<label>{{.i18n.Tr "admin.auths.group_search_base"}}</label>
+			<input name="group_dn" value="{{.group_dn}}" placeholder="e.g. ou=group,dc=mydomain,dc=com">
 		</div>
 		<div class="field">
-			<label for="group_filter">{{.i18n.Tr "admin.auths.valid_groups_filter"}}</label>
-			<input id="group_filter" name="group_filter" value="{{.group_filter}}" placeholder="e.g. (|(cn=gitea_users)(cn=admins))">
+			<label>{{.i18n.Tr "admin.auths.group_attribute_list_users"}}</label>
+			<input name="group_member_uid" value="{{.group_member_uid}}" placeholder="e.g. memberUid">
 		</div>
 		<div class="field">
-			<label for="group_member_uid">{{.i18n.Tr "admin.auths.group_attribute_list_users"}}</label>
-			<input id="group_member_uid" name="group_member_uid" value="{{.group_member_uid}}" placeholder="e.g. memberUid">
+			<label>{{.i18n.Tr "admin.auths.user_attribute_in_group"}}</label>
+			<input name="user_uid" value="{{.user_uid}}" placeholder="e.g. uid">
 		</div>
 		<div class="field">
-			<label for="user_uid">{{.i18n.Tr "admin.auths.user_attribute_in_group"}}</label>
-			<input id="user_uid" name="user_uid" value="{{.user_uid}}" placeholder="e.g. uid">
+			<label>{{.i18n.Tr "admin.auths.verify_group_membership"}}</label>
+			<input name="group_filter" value="{{.group_filter}}" placeholder="e.g. (|(cn=gitea_users)(cn=admins))">
+		</div>
+		<div class="field">
+			<label>{{.i18n.Tr "admin.auths.map_group_to_team"}}</label>
+			<input name="group_team_map" value="{{.group_team_map}}" placeholder='e.g. {"cn=my-group,cn=groups,dc=example,dc=org": {"MyGiteaOrganization": ["MyGiteaTeam1", "MyGiteaTeam2"]}}'>
+		</div>
+		<div class="ui checkbox">
+			<label>{{.i18n.Tr "admin.auths.map_group_to_team_removal"}}</label>
+			<input name="group_team_map_removal" type="checkbox" {{if .group_team_map_removal}}checked{{end}}>
 		</div>
-		<br/>
 	</div>
+	<!-- ldap group end -->
+
 	<div class="ldap inline field {{if not (eq .type 2)}}hide{{end}}">
 		<div class="ui checkbox">
 			<label for="use_paged_search"><strong>{{.i18n.Tr "admin.auths.use_paged_search"}}</strong></label>
diff --git a/web_src/js/features/admin-common.js b/web_src/js/features/admin-common.js
index d2021c45aa..2438fcf62b 100644
--- a/web_src/js/features/admin-common.js
+++ b/web_src/js/features/admin-common.js
@@ -91,12 +91,8 @@ export function initAdminCommon() {
     }
   }
 
-  function onVerifyGroupMembershipChange() {
-    if ($('#groups_enabled').is(':checked')) {
-      $('#groups_enabled_change').show();
-    } else {
-      $('#groups_enabled_change').hide();
-    }
+  function onEnableLdapGroupsChange() {
+    $('#ldap-group-options').toggle($('.js-ldap-group-toggle').is(':checked'));
   }
 
   // New authentication
@@ -139,7 +135,7 @@ export function initAdminCommon() {
       }
       if (authType === '2' || authType === '5') {
         onSecurityProtocolChange();
-        onVerifyGroupMembershipChange();
+        onEnableLdapGroupsChange();
       }
       if (authType === '2') {
         onUsePagedSearchChange();
@@ -150,15 +146,15 @@ export function initAdminCommon() {
     $('#use_paged_search').on('change', onUsePagedSearchChange);
     $('#oauth2_provider').on('change', () => onOAuth2Change(true));
     $('#oauth2_use_custom_url').on('change', () => onOAuth2UseCustomURLChange(true));
-    $('#groups_enabled').on('change', onVerifyGroupMembershipChange);
+    $('.js-ldap-group-toggle').on('change', onEnableLdapGroupsChange);
   }
   // Edit authentication
   if ($('.admin.edit.authentication').length > 0) {
     const authType = $('#auth_type').val();
     if (authType === '2' || authType === '5') {
       $('#security_protocol').on('change', onSecurityProtocolChange);
-      $('#groups_enabled').on('change', onVerifyGroupMembershipChange);
-      onVerifyGroupMembershipChange();
+      $('.js-ldap-group-toggle').on('change', onEnableLdapGroupsChange);
+      onEnableLdapGroupsChange();
       if (authType === '2') {
         $('#use_paged_search').on('change', onUsePagedSearchChange);
       }