diff --git a/dapps/valuetransfers/packages/tipmanager/events.go b/dapps/valuetransfers/packages/tipmanager/events.go
new file mode 100644
index 0000000000000000000000000000000000000000..96f8f7d6c232ec93798264f363b9b891e3c446d1
--- /dev/null
+++ b/dapps/valuetransfers/packages/tipmanager/events.go
@@ -0,0 +1,19 @@
+package tipmanager
+
+import (
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/branchmanager"
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload"
+	"github.com/iotaledger/hive.go/events"
+)
+
+// Events represents events happening on the TipManager.
+type Events struct {
+	// Fired when a tip is added.
+	TipAdded *events.Event
+	// Fired when a tip is removed.
+	TipRemoved *events.Event
+}
+
+func payloadIDEvent(handler interface{}, params ...interface{}) {
+	handler.(func(payload.ID, branchmanager.BranchID))(params[0].(payload.ID), params[1].(branchmanager.BranchID))
+}
diff --git a/dapps/valuetransfers/packages/tipmanager/tipmanager.go b/dapps/valuetransfers/packages/tipmanager/tipmanager.go
new file mode 100644
index 0000000000000000000000000000000000000000..a36f795a1627c67d35bc7bc4a8211c0efd218135
--- /dev/null
+++ b/dapps/valuetransfers/packages/tipmanager/tipmanager.go
@@ -0,0 +1,163 @@
+package tipmanager
+
+import (
+	"math/rand"
+	"sync"
+
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/branchmanager"
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload"
+	"github.com/iotaledger/goshimmer/packages/binary/datastructure"
+	"github.com/iotaledger/hive.go/events"
+)
+
+// TipManager manages tips of all branches and emits events for their removal and addition.
+type TipManager struct {
+	tips      map[branchmanager.BranchID]*datastructure.RandomMap
+	tipsMutex sync.Mutex
+	Events    Events
+}
+
+// New creates a new TipManager.
+func New() *TipManager {
+	return &TipManager{
+		tips: make(map[branchmanager.BranchID]*datastructure.RandomMap),
+		Events: Events{
+			TipAdded:   events.NewEvent(payloadIDEvent),
+			TipRemoved: events.NewEvent(payloadIDEvent),
+		},
+	}
+}
+
+// AddTip adds the given value object as a tip in the given branch.
+func (t *TipManager) AddTip(valueObject *payload.Payload, branch branchmanager.BranchID) {
+	objectID := valueObject.ID()
+	//parent1ID := valueObject.TrunkID()
+	//parent2ID := valueObject.BranchID()
+
+	t.tipsMutex.Lock()
+	defer t.tipsMutex.Unlock()
+
+	branchTips, ok := t.tips[branch]
+	if !ok {
+		// add new branch to tips map
+		branchTips = datastructure.NewRandomMap()
+		t.tips[branch] = branchTips
+	}
+
+	if branchTips.Set(objectID, objectID) {
+		t.Events.TipAdded.Trigger(objectID, branch)
+	}
+
+	// remove parents
+	// TODO: parents could be in another branch. get branch via ID ?
+	// utxoDAG.ValueObjectBooking(valueObjectID).Unwrap().BranchID()
+	//if _, deleted := branchTips.Delete(parent1ID); deleted {
+	//	t.Events.TipRemoved.Trigger(parent1ID, branch)
+	//}
+	//
+	//if _, deleted := branchTips.Delete(parent2ID); deleted {
+	//	t.Events.TipRemoved.Trigger(parent2ID, branch)
+	//}
+}
+
+// Tips randomly selects tips in the given branches weighted by size.
+func (t *TipManager) Tips(branches ...branchmanager.BranchID) (parent1ObjectID, parent2ObjectID payload.ID) {
+	if len(branches) == 0 {
+		parent1ObjectID = payload.GenesisID
+		parent2ObjectID = payload.GenesisID
+		return
+	}
+
+	weights := make([]int, len(branches))
+	totalTips := 0
+	tipsSlice := make([]*datastructure.RandomMap, len(branches))
+
+	t.tipsMutex.Lock()
+	defer t.tipsMutex.Unlock()
+
+	// prepare weighted random selection
+	for i, b := range branches {
+		branchTips, ok := t.tips[b]
+		if !ok {
+			continue
+		}
+
+		tipsSlice[i] = branchTips
+		weights[i] = branchTips.Size()
+		totalTips += weights[i]
+	}
+
+	// no tips in the given branches
+	// TODO: what exactly to do in this case?
+	if totalTips == 0 {
+		parent1ObjectID = payload.GenesisID
+		parent2ObjectID = payload.GenesisID
+		return
+	}
+
+	// select tips
+	random := weightedRandom(weights)
+
+	branchTips := tipsSlice[random]
+	tip := branchTips.RandomEntry()
+	if tip == nil {
+		// this case should never occur due to weighted random selection
+		panic("tip is nil.")
+	}
+	parent1ObjectID = tip.(payload.ID)
+
+	if totalTips == 1 {
+		parent2ObjectID = parent1ObjectID
+		return
+	}
+
+	// adjust weights
+	weights[random] -= 1
+
+	branchTips = tipsSlice[weightedRandom(weights)]
+	tip = branchTips.RandomEntry()
+	if tip == nil {
+		// this case should never occur due to weighted random selection
+		panic("tip is nil.")
+	}
+	parent2ObjectID = tip.(payload.ID)
+
+	return
+}
+
+// TipCount returns the total tips in the given branches.
+func (t *TipManager) TipCount(branches ...branchmanager.BranchID) int {
+	t.tipsMutex.Lock()
+	defer t.tipsMutex.Unlock()
+
+	totalTips := 0
+	for _, b := range branches {
+		branchTips, ok := t.tips[b]
+		if !ok {
+			continue
+		}
+
+		totalTips += branchTips.Size()
+	}
+	return totalTips
+}
+
+func weightedRandom(weights []int) int {
+	if len(weights) == 0 {
+		return 0
+	}
+
+	totalWeight := 0
+	for _, w := range weights {
+		totalWeight += w
+	}
+	r := rand.Intn(totalWeight)
+
+	for i, w := range weights {
+		r -= w
+		if r < 0 {
+			return i
+		}
+	}
+	return len(weights) - 1
+}
diff --git a/dapps/valuetransfers/packages/tipmanager/tipmanager_test.go b/dapps/valuetransfers/packages/tipmanager/tipmanager_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..926b7a8b91f06497c2c28f61d4a825c935aafe0d
--- /dev/null
+++ b/dapps/valuetransfers/packages/tipmanager/tipmanager_test.go
@@ -0,0 +1,130 @@
+package tipmanager
+
+import (
+	"math/rand"
+	"sync"
+	"testing"
+
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/address"
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/balance"
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/branchmanager"
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/payload"
+	"github.com/iotaledger/goshimmer/dapps/valuetransfers/packages/transaction"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestTipManagerSimple(t *testing.T) {
+	tipManager := New()
+
+	// check if first tips point to genesis
+	parent1, parent2 := tipManager.Tips()
+	assert.Equal(t, payload.GenesisID, parent1)
+	assert.Equal(t, payload.GenesisID, parent2)
+
+	// add few tips and check whether total tips count matches
+	totalTips := 100
+	branchesMap := make(map[branchmanager.BranchID]*payload.Payload, totalTips)
+	branches := make([]branchmanager.BranchID, totalTips)
+	for i := 0; i < totalTips; i++ {
+		branchID := branchmanager.NewBranchID(transaction.RandomID())
+		branches[i] = branchID
+		branchesMap[branchID] = payload.New(payload.GenesisID, payload.GenesisID, createDummyTransaction())
+		tipManager.AddTip(branchesMap[branchID], branchID)
+	}
+	assert.Equal(t, totalTips, tipManager.TipCount(branches...))
+
+	// check if tips point to genesis when branches are not found
+	parent1, parent2 = tipManager.Tips(branchmanager.UndefinedBranchID, branchmanager.NewBranchID(transaction.RandomID()))
+	assert.Equal(t, 0, tipManager.TipCount(branchmanager.UndefinedBranchID, branchmanager.NewBranchID(transaction.RandomID())))
+	assert.Equal(t, payload.GenesisID, parent1)
+	assert.Equal(t, payload.GenesisID, parent2)
+
+	// use each branch to get the single tip
+	for b, o := range branchesMap {
+		parent1ObjectID, parent2ObjectID := tipManager.Tips(b)
+		assert.Equal(t, o.ID(), parent1ObjectID)
+		assert.Equal(t, o.ID(), parent2ObjectID)
+	}
+
+	// select tips from two branches
+	parent1, parent2 = tipManager.Tips(branches[0], branches[1])
+	s := []payload.ID{
+		branchesMap[branches[0]].ID(),
+		branchesMap[branches[1]].ID(),
+	}
+	assert.Contains(t, s, parent1)
+	assert.Contains(t, s, parent2)
+}
+
+func TestTipManagerParallel(t *testing.T) {
+	totalTips := 1000
+	totalThreads := 100
+	totalBranches := 10
+
+	tipManager := New()
+
+	branches := make([]branchmanager.BranchID, totalBranches)
+	for i := 0; i < totalBranches; i++ {
+		branchID := branchmanager.NewBranchID(transaction.RandomID())
+		branches[i] = branchID
+	}
+
+	// add tips in parallel
+	var wg sync.WaitGroup
+	for i := 0; i < totalThreads; i++ {
+		wg.Add(1)
+		go func() {
+			defer wg.Done()
+
+			for t := 0; t < totalTips; t++ {
+				tip := payload.New(payload.GenesisID, payload.GenesisID, createDummyTransaction())
+				random := rand.Intn(totalBranches)
+				tipManager.AddTip(tip, branches[random])
+			}
+		}()
+	}
+	wg.Wait()
+
+	// check total tip count
+	assert.Equal(t, totalTips*totalThreads, tipManager.TipCount(branches...))
+}
+
+func TestTipManager_weightedRandom(t *testing.T) {
+	totalRounds := 1000000
+	weights := []int{1, 20, 39, 30, 9, 1}
+	weightsSum := 0
+	for _, w := range weights {
+		weightsSum += w
+	}
+	// make sure result is within delta of 0.01
+	delta := float64(totalRounds / weightsSum)
+
+	counts := make([]int, len(weights))
+
+	// calculate weighted random
+	for i := 0; i < totalRounds; i++ {
+		counts[weightedRandom(weights)]++
+	}
+
+	for i, c := range counts {
+		expected := totalRounds * weights[i] / weightsSum
+		assert.InDelta(t, expected, c, delta)
+	}
+}
+
+func createDummyTransaction() *transaction.Transaction {
+	return transaction.New(
+		// inputs
+		transaction.NewInputs(
+			transaction.NewOutputID(address.Random(), transaction.RandomID()),
+			transaction.NewOutputID(address.Random(), transaction.RandomID()),
+		),
+
+		// outputs
+		transaction.NewOutputs(map[address.Address][]*balance.Balance{
+			address.Random(): {
+				balance.New(balance.ColorIOTA, 1337),
+			},
+		}),
+	)
+}