diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go
index 3bf133f56222..c20a45ebc972 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -398,6 +398,11 @@ func SignUp(ctx *context.Context) {
 	// Show Disabled Registration message if DisableRegistration or AllowOnlyExternalRegistration options are true
 	ctx.Data["DisableRegistration"] = setting.Service.DisableRegistration || setting.Service.AllowOnlyExternalRegistration
 
+	redirectTo := ctx.FormString("redirect_to")
+	if len(redirectTo) > 0 {
+		middleware.SetRedirectToCookie(ctx.Resp, redirectTo)
+	}
+
 	ctx.HTML(http.StatusOK, tplSignUp)
 }
 
@@ -729,6 +734,12 @@ func handleAccountActivation(ctx *context.Context, user *user_model.User) {
 	}
 
 	ctx.Flash.Success(ctx.Tr("auth.account_activated"))
+	if redirectTo := ctx.GetSiteCookie("redirect_to"); len(redirectTo) > 0 {
+		middleware.DeleteRedirectToCookie(ctx.Resp)
+		ctx.RedirectToFirst(redirectTo)
+		return
+	}
+
 	ctx.Redirect(setting.AppSubURL + "/")
 }
 
diff --git a/services/auth/middleware.go b/services/auth/middleware.go
index d1955a4c9001..4a0b613fa662 100644
--- a/services/auth/middleware.go
+++ b/services/auth/middleware.go
@@ -120,9 +120,9 @@ func VerifyAuthWithOptions(options *VerifyOptions) func(ctx *context.Context) {
 			}
 		}
 
-		// Redirect to dashboard if user tries to visit any non-login page.
+		// Redirect to dashboard (or alternate location) if user tries to visit any non-login page.
 		if options.SignOutRequired && ctx.IsSigned && ctx.Req.URL.RequestURI() != "/" {
-			ctx.Redirect(setting.AppSubURL + "/")
+			ctx.RedirectToFirst(ctx.FormString("redirect_to"))
 			return
 		}
 
diff --git a/services/mailer/mail_team_invite.go b/services/mailer/mail_team_invite.go
index b6f47ee9215b..1403923c7954 100644
--- a/services/mailer/mail_team_invite.go
+++ b/services/mailer/mail_team_invite.go
@@ -6,6 +6,8 @@ package mailer
 import (
 	"bytes"
 	"context"
+	"fmt"
+	"net/url"
 
 	org_model "code.gitea.io/gitea/models/organization"
 	user_model "code.gitea.io/gitea/models/user"
@@ -33,6 +35,22 @@ func MailTeamInvite(ctx context.Context, inviter *user_model.User, team *org_mod
 
 	locale := translation.NewLocale(inviter.Language)
 
+	// check if a user with this email already exists
+	user, err := user_model.GetUserByEmail(ctx, invite.Email)
+	if err != nil && !user_model.IsErrUserNotExist(err) {
+		return err
+	} else if user != nil && user.ProhibitLogin {
+		return fmt.Errorf("login is prohibited for the invited user")
+	}
+
+	inviteRedirect := url.QueryEscape(fmt.Sprintf("/org/invite/%s", invite.Token))
+	inviteURL := fmt.Sprintf("%suser/sign_up?redirect_to=%s", setting.AppURL, inviteRedirect)
+
+	if err == nil && user != nil {
+		// user account exists
+		inviteURL = fmt.Sprintf("%suser/login?redirect_to=%s", setting.AppURL, inviteRedirect)
+	}
+
 	subject := locale.Tr("mail.team_invite.subject", inviter.DisplayName(), org.DisplayName())
 	mailMeta := map[string]any{
 		"Inviter":      inviter,
@@ -40,6 +58,7 @@ func MailTeamInvite(ctx context.Context, inviter *user_model.User, team *org_mod
 		"Team":         team,
 		"Invite":       invite,
 		"Subject":      subject,
+		"InviteURL":    inviteURL,
 		// helper
 		"locale":    locale,
 		"Str2html":  templates.Str2html,
diff --git a/templates/mail/team_invite.tmpl b/templates/mail/team_invite.tmpl
index 835789526539..d21b7843ec78 100644
--- a/templates/mail/team_invite.tmpl
+++ b/templates/mail/team_invite.tmpl
@@ -4,10 +4,9 @@
 	<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
 	<meta name="format-detection" content="telephone=no,date=no,address=no,email=no,url=no">
 </head>
-{{$invite_url := printf "%sorg/invite/%s" AppUrl (QueryEscape .Invite.Token)}}
 <body>
 	<p>{{.locale.Tr "mail.team_invite.text_1" (DotEscape .Inviter.DisplayName) (DotEscape .Team.Name) (DotEscape .Organization.DisplayName) | Str2html}}</p>
-	<p>{{.locale.Tr "mail.team_invite.text_2"}}</p><p><a href="{{$invite_url}}">{{$invite_url}}</a></p>
+	<p>{{.locale.Tr "mail.team_invite.text_2"}}</p><p><a href="{{.InviteURL}}">{{.InviteURL}}</a></p>
 	<p>{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
 	<p>{{.locale.Tr "mail.team_invite.text_3" .Invite.Email}}</p>
 
diff --git a/tests/integration/org_team_invite_test.go b/tests/integration/org_team_invite_test.go
index 4d848dfc6034..919769a61a25 100644
--- a/tests/integration/org_team_invite_test.go
+++ b/tests/integration/org_team_invite_test.go
@@ -6,6 +6,8 @@ package integration
 import (
 	"fmt"
 	"net/http"
+	"net/url"
+	"strings"
 	"testing"
 
 	"code.gitea.io/gitea/models/db"
@@ -37,9 +39,9 @@ func TestOrgTeamEmailInvite(t *testing.T) {
 
 	session := loginUser(t, "user1")
 
-	url := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name)
-	csrf := GetCSRF(t, session, url)
-	req := NewRequestWithValues(t, "POST", url+"/action/add", map[string]string{
+	teamURL := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name)
+	csrf := GetCSRF(t, session, teamURL)
+	req := NewRequestWithValues(t, "POST", teamURL+"/action/add", map[string]string{
 		"_csrf": csrf,
 		"uid":   "1",
 		"uname": user.Email,
@@ -56,9 +58,9 @@ func TestOrgTeamEmailInvite(t *testing.T) {
 	session = loginUser(t, user.Name)
 
 	// join the team
-	url = fmt.Sprintf("/org/invite/%s", invites[0].Token)
-	csrf = GetCSRF(t, session, url)
-	req = NewRequestWithValues(t, "POST", url, map[string]string{
+	inviteURL := fmt.Sprintf("/org/invite/%s", invites[0].Token)
+	csrf = GetCSRF(t, session, inviteURL)
+	req = NewRequestWithValues(t, "POST", inviteURL, map[string]string{
 		"_csrf": csrf,
 	})
 	resp = session.MakeRequest(t, req, http.StatusSeeOther)
@@ -69,3 +71,308 @@ func TestOrgTeamEmailInvite(t *testing.T) {
 	assert.NoError(t, err)
 	assert.True(t, isMember)
 }
+
+// Check that users are redirected to accept the invitation correctly after login
+func TestOrgTeamEmailInviteRedirectsExistingUser(t *testing.T) {
+	if setting.MailService == nil {
+		t.Skip()
+		return
+	}
+
+	defer tests.PrepareTestEnv(t)()
+
+	org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
+	team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
+
+	isMember, err := organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, user.ID)
+	assert.NoError(t, err)
+	assert.False(t, isMember)
+
+	// create the invite
+	session := loginUser(t, "user1")
+
+	teamURL := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name)
+	req := NewRequestWithValues(t, "POST", teamURL+"/action/add", map[string]string{
+		"_csrf": GetCSRF(t, session, teamURL),
+		"uid":   "1",
+		"uname": user.Email,
+	})
+	resp := session.MakeRequest(t, req, http.StatusSeeOther)
+	req = NewRequest(t, "GET", test.RedirectURL(resp))
+	session.MakeRequest(t, req, http.StatusOK)
+
+	// get the invite token
+	invites, err := organization.GetInvitesByTeamID(db.DefaultContext, team.ID)
+	assert.NoError(t, err)
+	assert.Len(t, invites, 1)
+
+	// accept the invite
+	inviteURL := fmt.Sprintf("/org/invite/%s", invites[0].Token)
+	req = NewRequest(t, "GET", fmt.Sprintf("/user/login?redirect_to=%s", url.QueryEscape(inviteURL)))
+	resp = MakeRequest(t, req, http.StatusOK)
+
+	doc := NewHTMLParser(t, resp.Body)
+	req = NewRequestWithValues(t, "POST", "/user/login", map[string]string{
+		"_csrf":     doc.GetCSRF(),
+		"user_name": "user5",
+		"password":  "password",
+	})
+	for _, c := range resp.Result().Cookies() {
+		req.AddCookie(c)
+	}
+
+	resp = MakeRequest(t, req, http.StatusSeeOther)
+	assert.Equal(t, inviteURL, test.RedirectURL(resp))
+
+	// complete the login process
+	ch := http.Header{}
+	ch.Add("Cookie", strings.Join(resp.Header()["Set-Cookie"], ";"))
+	cr := http.Request{Header: ch}
+
+	session = emptyTestSession(t)
+	baseURL, err := url.Parse(setting.AppURL)
+	assert.NoError(t, err)
+	session.jar.SetCookies(baseURL, cr.Cookies())
+
+	// make the request
+	req = NewRequestWithValues(t, "POST", test.RedirectURL(resp), map[string]string{
+		"_csrf": GetCSRF(t, session, test.RedirectURL(resp)),
+	})
+	resp = session.MakeRequest(t, req, http.StatusSeeOther)
+	req = NewRequest(t, "GET", test.RedirectURL(resp))
+	session.MakeRequest(t, req, http.StatusOK)
+
+	isMember, err = organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, user.ID)
+	assert.NoError(t, err)
+	assert.True(t, isMember)
+}
+
+// Check that newly signed up users are redirected to accept the invitation correctly
+func TestOrgTeamEmailInviteRedirectsNewUser(t *testing.T) {
+	if setting.MailService == nil {
+		t.Skip()
+		return
+	}
+
+	defer tests.PrepareTestEnv(t)()
+
+	org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
+	team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
+
+	// create the invite
+	session := loginUser(t, "user1")
+
+	teamURL := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name)
+	req := NewRequestWithValues(t, "POST", teamURL+"/action/add", map[string]string{
+		"_csrf": GetCSRF(t, session, teamURL),
+		"uid":   "1",
+		"uname": "doesnotexist@example.com",
+	})
+	resp := session.MakeRequest(t, req, http.StatusSeeOther)
+	req = NewRequest(t, "GET", test.RedirectURL(resp))
+	session.MakeRequest(t, req, http.StatusOK)
+
+	// get the invite token
+	invites, err := organization.GetInvitesByTeamID(db.DefaultContext, team.ID)
+	assert.NoError(t, err)
+	assert.Len(t, invites, 1)
+
+	// accept the invite
+	inviteURL := fmt.Sprintf("/org/invite/%s", invites[0].Token)
+	req = NewRequest(t, "GET", fmt.Sprintf("/user/sign_up?redirect_to=%s", url.QueryEscape(inviteURL)))
+	resp = MakeRequest(t, req, http.StatusOK)
+
+	doc := NewHTMLParser(t, resp.Body)
+	req = NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{
+		"_csrf":     doc.GetCSRF(),
+		"user_name": "doesnotexist",
+		"email":     "doesnotexist@example.com",
+		"password":  "examplePassword!1",
+		"retype":    "examplePassword!1",
+	})
+	for _, c := range resp.Result().Cookies() {
+		req.AddCookie(c)
+	}
+
+	resp = MakeRequest(t, req, http.StatusSeeOther)
+	assert.Equal(t, inviteURL, test.RedirectURL(resp))
+
+	// complete the signup process
+	ch := http.Header{}
+	ch.Add("Cookie", strings.Join(resp.Header()["Set-Cookie"], ";"))
+	cr := http.Request{Header: ch}
+
+	session = emptyTestSession(t)
+	baseURL, err := url.Parse(setting.AppURL)
+	assert.NoError(t, err)
+	session.jar.SetCookies(baseURL, cr.Cookies())
+
+	// make the redirected request
+	req = NewRequestWithValues(t, "POST", test.RedirectURL(resp), map[string]string{
+		"_csrf": GetCSRF(t, session, test.RedirectURL(resp)),
+	})
+	resp = session.MakeRequest(t, req, http.StatusSeeOther)
+	req = NewRequest(t, "GET", test.RedirectURL(resp))
+	session.MakeRequest(t, req, http.StatusOK)
+
+	// get the new user
+	newUser, err := user_model.GetUserByName(db.DefaultContext, "doesnotexist")
+	assert.NoError(t, err)
+
+	isMember, err := organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, newUser.ID)
+	assert.NoError(t, err)
+	assert.True(t, isMember)
+}
+
+// Check that users are redirected correctly after confirming their email
+func TestOrgTeamEmailInviteRedirectsNewUserWithActivation(t *testing.T) {
+	if setting.MailService == nil {
+		t.Skip()
+		return
+	}
+
+	// enable email confirmation temporarily
+	defer func(prevVal bool) {
+		setting.Service.RegisterEmailConfirm = prevVal
+	}(setting.Service.RegisterEmailConfirm)
+	setting.Service.RegisterEmailConfirm = true
+
+	defer tests.PrepareTestEnv(t)()
+
+	org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
+	team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
+
+	// create the invite
+	session := loginUser(t, "user1")
+
+	teamURL := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name)
+	req := NewRequestWithValues(t, "POST", teamURL+"/action/add", map[string]string{
+		"_csrf": GetCSRF(t, session, teamURL),
+		"uid":   "1",
+		"uname": "doesnotexist@example.com",
+	})
+	resp := session.MakeRequest(t, req, http.StatusSeeOther)
+	req = NewRequest(t, "GET", test.RedirectURL(resp))
+	session.MakeRequest(t, req, http.StatusOK)
+
+	// get the invite token
+	invites, err := organization.GetInvitesByTeamID(db.DefaultContext, team.ID)
+	assert.NoError(t, err)
+	assert.Len(t, invites, 1)
+
+	// accept the invite
+	inviteURL := fmt.Sprintf("/org/invite/%s", invites[0].Token)
+	req = NewRequest(t, "GET", fmt.Sprintf("/user/sign_up?redirect_to=%s", url.QueryEscape(inviteURL)))
+	inviteResp := MakeRequest(t, req, http.StatusOK)
+
+	doc := NewHTMLParser(t, resp.Body)
+	req = NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{
+		"_csrf":     doc.GetCSRF(),
+		"user_name": "doesnotexist",
+		"email":     "doesnotexist@example.com",
+		"password":  "examplePassword!1",
+		"retype":    "examplePassword!1",
+	})
+	for _, c := range inviteResp.Result().Cookies() {
+		req.AddCookie(c)
+	}
+
+	resp = MakeRequest(t, req, http.StatusOK)
+
+	user, err := user_model.GetUserByName(db.DefaultContext, "doesnotexist")
+	assert.NoError(t, err)
+
+	ch := http.Header{}
+	ch.Add("Cookie", strings.Join(resp.Header()["Set-Cookie"], ";"))
+	cr := http.Request{Header: ch}
+
+	session = emptyTestSession(t)
+	baseURL, err := url.Parse(setting.AppURL)
+	assert.NoError(t, err)
+	session.jar.SetCookies(baseURL, cr.Cookies())
+
+	activateURL := fmt.Sprintf("/user/activate?code=%s", user.GenerateEmailActivateCode("doesnotexist@example.com"))
+	req = NewRequestWithValues(t, "POST", activateURL, map[string]string{
+		"password": "examplePassword!1",
+	})
+
+	// use the cookies set by the signup request
+	for _, c := range inviteResp.Result().Cookies() {
+		req.AddCookie(c)
+	}
+
+	resp = session.MakeRequest(t, req, http.StatusSeeOther)
+	// should be redirected to accept the invite
+	assert.Equal(t, inviteURL, test.RedirectURL(resp))
+
+	req = NewRequestWithValues(t, "POST", test.RedirectURL(resp), map[string]string{
+		"_csrf": GetCSRF(t, session, test.RedirectURL(resp)),
+	})
+	resp = session.MakeRequest(t, req, http.StatusSeeOther)
+	req = NewRequest(t, "GET", test.RedirectURL(resp))
+	session.MakeRequest(t, req, http.StatusOK)
+
+	isMember, err := organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, user.ID)
+	assert.NoError(t, err)
+	assert.True(t, isMember)
+}
+
+// Test that a logged-in user who navigates to the sign-up link is then redirected using redirect_to
+// For example: an invite may have been created before the user account was created, but they may be
+// accepting the invite after having created an account separately
+func TestOrgTeamEmailInviteRedirectsExistingUserWithLogin(t *testing.T) {
+	if setting.MailService == nil {
+		t.Skip()
+		return
+	}
+
+	defer tests.PrepareTestEnv(t)()
+
+	org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
+	team := unittest.AssertExistsAndLoadBean(t, &organization.Team{ID: 2})
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 5})
+
+	isMember, err := organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, user.ID)
+	assert.NoError(t, err)
+	assert.False(t, isMember)
+
+	// create the invite
+	session := loginUser(t, "user1")
+
+	teamURL := fmt.Sprintf("/org/%s/teams/%s", org.Name, team.Name)
+	req := NewRequestWithValues(t, "POST", teamURL+"/action/add", map[string]string{
+		"_csrf": GetCSRF(t, session, teamURL),
+		"uid":   "1",
+		"uname": user.Email,
+	})
+	resp := session.MakeRequest(t, req, http.StatusSeeOther)
+	req = NewRequest(t, "GET", test.RedirectURL(resp))
+	session.MakeRequest(t, req, http.StatusOK)
+
+	// get the invite token
+	invites, err := organization.GetInvitesByTeamID(db.DefaultContext, team.ID)
+	assert.NoError(t, err)
+	assert.Len(t, invites, 1)
+
+	// note: the invited user has logged in
+	session = loginUser(t, "user5")
+
+	// accept the invite (note: this uses the sign_up url)
+	inviteURL := fmt.Sprintf("/org/invite/%s", invites[0].Token)
+	req = NewRequest(t, "GET", fmt.Sprintf("/user/sign_up?redirect_to=%s", url.QueryEscape(inviteURL)))
+	resp = session.MakeRequest(t, req, http.StatusSeeOther)
+	assert.Equal(t, inviteURL, test.RedirectURL(resp))
+
+	// make the request
+	req = NewRequestWithValues(t, "POST", test.RedirectURL(resp), map[string]string{
+		"_csrf": GetCSRF(t, session, test.RedirectURL(resp)),
+	})
+	resp = session.MakeRequest(t, req, http.StatusSeeOther)
+	req = NewRequest(t, "GET", test.RedirectURL(resp))
+	session.MakeRequest(t, req, http.StatusOK)
+
+	isMember, err = organization.IsTeamMember(db.DefaultContext, team.OrgID, team.ID, user.ID)
+	assert.NoError(t, err)
+	assert.True(t, isMember)
+}